From 6b000998645057a2a17ce0381dc9e615295eef97 Mon Sep 17 00:00:00 2001 From: andream16 Date: Sun, 15 Dec 2024 13:58:04 +0000 Subject: [PATCH] Open sourcing V1 types. --- smithyctl/types/v1/component.go | 38 ++++++ smithyctl/types/v1/component_enum.go | 95 +++++++++++++++ smithyctl/types/v1/parameter.go | 142 ++++++++++++++++++++++ smithyctl/types/v1/parameter_enum.go | 49 ++++++++ smithyctl/types/v1/parameter_test.go | 171 +++++++++++++++++++++++++++ smithyctl/types/v1/workflow.go | 29 +++++ 6 files changed, 524 insertions(+) create mode 100644 smithyctl/types/v1/component.go create mode 100644 smithyctl/types/v1/component_enum.go create mode 100644 smithyctl/types/v1/parameter.go create mode 100644 smithyctl/types/v1/parameter_enum.go create mode 100644 smithyctl/types/v1/parameter_test.go create mode 100644 smithyctl/types/v1/workflow.go diff --git a/smithyctl/types/v1/component.go b/smithyctl/types/v1/component.go new file mode 100644 index 000000000..20e029e2a --- /dev/null +++ b/smithyctl/types/v1/component.go @@ -0,0 +1,38 @@ +package v1 + +type ( + // ComponentType represents all the types of components that Smithy supports. + // ENUM(unknown, target, scanner, enricher, filter, reporter) + ComponentType string + + // Component is a binary or a script that can be used in the context of a + // workflow to generate some useful result. + Component struct { + // Description describes what the component will do. + Description string + // Name is the component name. + Name string + // Parameters is the list of parameters to be supplied to the component. + Parameters []Parameter + // Steps is the list of steps to be supplied to the component. + Steps []Step + // Type represents the component type. + Type ComponentType + } + + // Step represents an executing step inside a Component. + Step struct { + // Args is the list of arguments supplied to a component. + Args []string + // EnvVars is the set of environment variables key:val supplied to a component. + EnvVars map[string]string + // Name is the step name. + Name string + // Executable is the path to the entrypoint to run the component. + Executable string + // Images is the docker image to be run for this component. + Image string + // Script will be deprecated after rewriting all components. + Script string + } +) diff --git a/smithyctl/types/v1/component_enum.go b/smithyctl/types/v1/component_enum.go new file mode 100644 index 000000000..a5816afa5 --- /dev/null +++ b/smithyctl/types/v1/component_enum.go @@ -0,0 +1,95 @@ +// Code generated by go-enum DO NOT EDIT. +// Version: +// Revision: +// Build Date: +// Built By: + +package v1 + +import ( + "errors" + "fmt" +) + +const ( + // ComponentTypeUnknown is a ComponentType of type unknown. + ComponentTypeUnknown ComponentType = "unknown" + // ComponentTypeTarget is a ComponentType of type target. + ComponentTypeTarget ComponentType = "target" + // ComponentTypeScanner is a ComponentType of type scanner. + ComponentTypeScanner ComponentType = "scanner" + // ComponentTypeEnricher is a ComponentType of type enricher. + ComponentTypeEnricher ComponentType = "enricher" + // ComponentTypeFilter is a ComponentType of type filter. + ComponentTypeFilter ComponentType = "filter" + // ComponentTypeReporter is a ComponentType of type reporter. + ComponentTypeReporter ComponentType = "reporter" +) + +var ErrInvalidComponentType = errors.New("not a valid ComponentType") + +// String implements the Stringer interface. +func (x ComponentType) String() string { + return string(x) +} + +// IsValid provides a quick way to determine if the typed value is +// part of the allowed enumerated values +func (x ComponentType) IsValid() bool { + _, err := ParseComponentType(string(x)) + return err == nil +} + +var _ComponentTypeValue = map[string]ComponentType{ + "unknown": ComponentTypeUnknown, + "target": ComponentTypeTarget, + "scanner": ComponentTypeScanner, + "enricher": ComponentTypeEnricher, + "filter": ComponentTypeFilter, + "reporter": ComponentTypeReporter, +} + +// ParseComponentType attempts to convert a string to a ComponentType. +func ParseComponentType(name string) (ComponentType, error) { + if x, ok := _ComponentTypeValue[name]; ok { + return x, nil + } + return ComponentType(""), fmt.Errorf("%s is %w", name, ErrInvalidComponentType) +} + +const ( + // StepTypeUnknown is a StepType of type unknown. + StepTypeUnknown StepType = "unknown" + // StepTypeContainer is a StepType of type container. + StepTypeContainer StepType = "container" + // StepTypeBinary is a StepType of type binary. + StepTypeBinary StepType = "binary" +) + +var ErrInvalidStepType = errors.New("not a valid StepType") + +// String implements the Stringer interface. +func (x StepType) String() string { + return string(x) +} + +// IsValid provides a quick way to determine if the typed value is +// part of the allowed enumerated values +func (x StepType) IsValid() bool { + _, err := ParseStepType(string(x)) + return err == nil +} + +var _StepTypeValue = map[string]StepType{ + "unknown": StepTypeUnknown, + "container": StepTypeContainer, + "binary": StepTypeBinary, +} + +// ParseStepType attempts to convert a string to a StepType. +func ParseStepType(name string) (StepType, error) { + if x, ok := _StepTypeValue[name]; ok { + return x, nil + } + return StepType(""), fmt.Errorf("%s is %w", name, ErrInvalidStepType) +} diff --git a/smithyctl/types/v1/parameter.go b/smithyctl/types/v1/parameter.go new file mode 100644 index 000000000..673a7bb9e --- /dev/null +++ b/smithyctl/types/v1/parameter.go @@ -0,0 +1,142 @@ +package v1 + +import ( + "encoding/json" + "errors" + "fmt" +) + +var ( + // ErrMismatchedExpectedType is returned when the type field of a parameter + // is not the same as the actual type of the value of a parameter. + ErrMismatchedExpectedType = errors.New("stated parameter type different than actual value type") + // ErrUnknownParameterType is returned when the type of field parameter + // is not one of the expected values. + ErrUnknownParameterType = errors.New("unknown parameter type") +) + +type ( + // ParameterType represents the type of parameter that can be parsed by the + // system. + // ENUM(string, const:string, list:string) + ParameterType string + + // Parameter is a struct whose value must be of the type as it's defined in the + // Type field. Due to the fact that this value is expected to be communicated + // to external clients via JSON, which doesn't support rich types, we need to + // communicate the expected value type via an enum string. Given the Golang + // type system, there is no way to enforce the type restrictions via an + // interface, so do all the type checks when marshaling/unmarshalling the JSON + // bytes, since this type will constantly be subject to such transformations. + Parameter struct { + // Name is the name of the parameter. + Name string + // Type is the parameter type. + Type ParameterType + // Value is the JSON encoded/decoded value of the parameter which is decoded based its Type. + Value any + } +) + +// UnmarshalJSON unmarshal JSON bytes into a Parameter object. +func (p *Parameter) UnmarshalJSON(b []byte) error { + partialParameter := &struct { + Name string + Type ParameterType + }{} + + var err error + if err = json.Unmarshal(b, partialParameter); err != nil { + return err + } + + p.Name = partialParameter.Name + p.Type = partialParameter.Type + + switch partialParameter.Type { + case ParameterTypeString, ParameterTypeConststring: + parameterValueStrPtr := &struct{ Value *string }{} + if err = json.Unmarshal(b, parameterValueStrPtr); err == nil { + p.Value = parameterValueStrPtr.Value + break + } + parameterValueStr := &struct{ Value string }{} + err = json.Unmarshal(b, parameterValueStr) + p.Value = parameterValueStr.Value + case ParameterTypeListstring: + parameterValue := &struct{ Value []string }{} + err = json.Unmarshal(b, parameterValue) + p.Value = parameterValue.Value + default: + err = ErrUnknownParameterType + } + if err != nil { + return fmt.Errorf("parameter.Name: %s, parameter.Type: %s: %w", partialParameter.Name, partialParameter.Type, err) + } + + return nil +} + +// MarshalJSON marshals the Parameter into JSON bytes. +func (p *Parameter) MarshalJSON() ([]byte, error) { + if err := p.Validate(); err != nil { + return nil, err + } + + b, err := json.Marshal(struct { + Name string + Type ParameterType + Value any + }{ + Name: p.Name, + Type: p.Type, + Value: p.Value, + }) + if err != nil { + return nil, fmt.Errorf("could not json marshal value: %w", err) + } + + return b, nil +} + +// Validate checks all the fields of the parameter to make sure that the type +// specified matches the actual type of the value +func (p *Parameter) Validate() error { + _, err := ParseParameterType(string(p.Type)) + if err != nil { + return fmt.Errorf("could not parse parameter '%s' with type: '%s': %w", p.Name, p.Type, err) + } + + if p.Value == nil { + return nil + } + + var correctType bool + switch p.Type { + case ParameterTypeString, ParameterTypeConststring: + _, correctType = p.Value.(*string) + if !correctType { + _, correctType = p.Value.(string) + } + case ParameterTypeListstring: + _, correctType = p.Value.([]string) + default: + err = ErrUnknownParameterType + } + + if !correctType { + err = ErrMismatchedExpectedType + } + + if err != nil { + return fmt.Errorf( + "invalid parameter '%s' with type '%s' and value '%v': %w", + p.Name, + p.Type, + p.Value, + err, + ) + } + + return nil +} diff --git a/smithyctl/types/v1/parameter_enum.go b/smithyctl/types/v1/parameter_enum.go new file mode 100644 index 000000000..60735cade --- /dev/null +++ b/smithyctl/types/v1/parameter_enum.go @@ -0,0 +1,49 @@ +// Code generated by go-enum DO NOT EDIT. +// Version: +// Revision: +// Build Date: +// Built By: + +package v1 + +import ( + "errors" + "fmt" +) + +const ( + // ParameterTypeString is a ParameterType of type string. + ParameterTypeString ParameterType = "string" + // ParameterTypeConststring is a ParameterType of type const:string. + ParameterTypeConststring ParameterType = "const:string" + // ParameterTypeListstring is a ParameterType of type list:string. + ParameterTypeListstring ParameterType = "list:string" +) + +var ErrInvalidParameterType = errors.New("not a valid ParameterType") + +// String implements the Stringer interface. +func (x ParameterType) String() string { + return string(x) +} + +// IsValid provides a quick way to determine if the typed value is +// part of the allowed enumerated values +func (x ParameterType) IsValid() bool { + _, err := ParseParameterType(string(x)) + return err == nil +} + +var _ParameterTypeValue = map[string]ParameterType{ + "string": ParameterTypeString, + "const:string": ParameterTypeConststring, + "list:string": ParameterTypeListstring, +} + +// ParseParameterType attempts to convert a string to a ParameterType. +func ParseParameterType(name string) (ParameterType, error) { + if x, ok := _ParameterTypeValue[name]; ok { + return x, nil + } + return ParameterType(""), fmt.Errorf("%s is %w", name, ErrInvalidParameterType) +} diff --git a/smithyctl/types/v1/parameter_test.go b/smithyctl/types/v1/parameter_test.go new file mode 100644 index 000000000..8c95e7a95 --- /dev/null +++ b/smithyctl/types/v1/parameter_test.go @@ -0,0 +1,171 @@ +package v1_test + +import ( + "reflect" + "slices" + "testing" + + v1 "github.com/smithy-security/smithyctl/types/v1" +) + +func ptr[T any](v T) *T { + return &v +} + +func TestParameter(t *testing.T) { + t.Parallel() + + for _, tt := range []struct { + testCase string + param v1.Parameter + expectsMarshalErr bool + expectsUnmarshalErr bool + }{ + { + testCase: "it should marshal correctly a non empty pointer to a string", + param: v1.Parameter{ + Name: "non-empty-ptr-str", + Type: v1.ParameterTypeString, + Value: ptr("smithy"), + }, + }, + { + testCase: "it should marshal correctly an empty pointer to a string", + param: v1.Parameter{ + Name: "empty-ptr-str", + Type: v1.ParameterTypeString, + Value: "", + }, + }, + { + testCase: "it should marshal correctly a non empty string", + param: v1.Parameter{ + Name: "non-empty-str", + Type: v1.ParameterTypeString, + Value: "smithy", + }, + }, + { + testCase: "it should marshal correctly an empty string", + param: v1.Parameter{ + Name: "empty-str", + Type: v1.ParameterTypeString, + Value: ptr(""), + }, + }, + { + testCase: "it should marshal correctly an nil string", + param: v1.Parameter{ + Name: "nil-str", + Type: v1.ParameterTypeString, + }, + }, + { + testCase: "it should marshal correctly a non empty const pointer to a string", + param: v1.Parameter{ + Name: "non-empty-const-ptr-str", + Type: v1.ParameterTypeConststring, + Value: ptr("{{.Helm.template.value}}"), + }, + }, + { + testCase: "it should marshal correctly an empty const pointer to a string", + param: v1.Parameter{ + Name: "empty-const-ptr-str", + Type: v1.ParameterTypeConststring, + Value: ptr(""), + }, + }, + { + testCase: "it should marshal correctly a non empty const string", + param: v1.Parameter{ + Name: "non-empty-const-ptr-str", + Type: v1.ParameterTypeConststring, + Value: "{{.Helm.template.value}}", + }, + }, + { + testCase: "it should marshal correctly an empty string", + param: v1.Parameter{ + Name: "empty-const-ptr-str", + Type: v1.ParameterTypeConststring, + Value: "", + }, + }, + { + testCase: "it should marshal correctly an nil const string", + param: v1.Parameter{ + Name: "nil-const-str", + Type: v1.ParameterTypeConststring, + }, + }, + { + testCase: "it should marshal correctly a non empty list string", + param: v1.Parameter{ + Name: "non-empty-list-str", + Type: v1.ParameterTypeListstring, + Value: []string{"dracon", "is", "not", "smithy"}, + }, + }, + { + testCase: "it should marshal correctly an empty list string", + param: v1.Parameter{ + Name: "empty-list-str", + Type: v1.ParameterTypeListstring, + Value: make([]string, 0), + }, + }, + { + testCase: "it should marshal correctly an nil list string", + param: v1.Parameter{ + Name: "nil-list-str", + Type: v1.ParameterTypeListstring, + }, + }, + } { + t.Run(tt.testCase, func(t *testing.T) { + bb, err := tt.param.MarshalJSON() + if tt.expectsMarshalErr && err == nil { + t.Fatalf("expected marshal error but got nil") + } else if !tt.expectsMarshalErr && err != nil { + t.Fatalf("expected no marshal error but got %v", err) + } + + var param v1.Parameter + err = param.UnmarshalJSON(bb) + if tt.expectsUnmarshalErr && err == nil { + t.Fatalf("expected unmarshal error but got nil") + } else if !tt.expectsUnmarshalErr && err != nil { + t.Fatalf("expected no unmarshal error but got %v", err) + } + + if tt.param.Type != v1.ParameterTypeListstring { + switch { + case tt.param.Type != param.Type: + t.Fatalf("expected param type %v but got %v", tt.param.Type, param.Type) + case tt.param.Name != param.Name: + t.Fatalf("expected param name %v but got %v", tt.param.Name, param.Name) + default: + // A string can be a pointer. + if reflect.TypeOf(param.Value).Kind() == reflect.Ptr { + if tt.param.Value != nil && param.Value != nil && *tt.param.Value.(*string) != *param.Value.(*string) { + t.Fatalf("expected param value %v but got %v", tt.param.Value, param.Value) + } + // Or not. + } else { + if tt.param.Value != param.Value { + t.Fatalf("expected param value %v but got %v", tt.param.Value, param.Value) + } + } + } + // In case of lists, we need to check element by element. + } else if tt.param.Type == v1.ParameterTypeListstring && tt.param.Value != nil { + for _, str := range param.Value.([]string) { + if !slices.Contains(tt.param.Value.([]string), str) { + t.Fatalf("expected list element '%s' to be found in slice '%v'", str, tt.param.Value) + } + } + } + }) + } +} diff --git a/smithyctl/types/v1/workflow.go b/smithyctl/types/v1/workflow.go new file mode 100644 index 000000000..b6a62ba8a --- /dev/null +++ b/smithyctl/types/v1/workflow.go @@ -0,0 +1,29 @@ +package v1 + +type ( + // Workflow represents a combination of + Workflow struct { + // Description described what the workflow is configured for. + Description string + // Name is the name of the workflow. + Name string + // Stages contains the stages to be applied by the workflow. + Stages []Stage + } + + // Stage is a group of Components that can be executed in parallel during in + // the context of a Workflow Instance. + Stage struct { + // ComponentRefs contains the list of component references attached to this stage. + ComponentRefs []ComponentRef + } + + // ComponentRef represents a reference to a component along with some overrides + // that the user needs to declare. + ComponentRef struct { + // Component represents the linked component. + Component Component + // Overrides contains first later of parameter overrides for this component. + Overrides []Parameter + } +)