diff --git a/cmd/argo-actions/main.go b/cmd/argo-actions/main.go index 097a6f34c..c441e44a8 100644 --- a/cmd/argo-actions/main.go +++ b/cmd/argo-actions/main.go @@ -8,6 +8,7 @@ import ( "capact.io/capact/internal/logger" argoactions "capact.io/capact/pkg/argo-actions" + hubclient "capact.io/capact/pkg/hub/client" "capact.io/capact/pkg/hub/client/local" "capact.io/capact/pkg/hub/client/public" @@ -42,6 +43,12 @@ func main() { localClient := local.NewDefaultClient(cfg.LocalHubEndpoint) publicClient := public.NewDefaultClient(cfg.PublicHubEndpoint) + // TODO: Consider using connection `hubclient.New` and route requests through Gateway + client := hubclient.Client{ + Local: localClient, + Public: publicClient, + } + switch cfg.Action { case argoactions.DownloadAction: log := logger.With(zap.String("Action", argoactions.DownloadAction)) @@ -49,11 +56,11 @@ func main() { case argoactions.UploadAction: log := logger.With(zap.String("Action", argoactions.UploadAction)) - action = argoactions.NewUploadAction(log, localClient, publicClient, cfg.UploadConfig) + action = argoactions.NewUploadAction(log, &client, cfg.UploadConfig) case argoactions.UpdateAction: log := logger.With(zap.String("Action", argoactions.UpdateAction)) - action = argoactions.NewUpdateAction(log, localClient, publicClient, cfg.UpdateConfig) + action = argoactions.NewUpdateAction(log, &client, cfg.UpdateConfig) default: err := fmt.Errorf("Invalid action: %s", cfg.Action) diff --git a/cmd/cli/cmd/typeinstance/create.go b/cmd/cli/cmd/typeinstance/create.go index 40ce9219a..35ab9a40d 100644 --- a/cmd/cli/cmd/typeinstance/create.go +++ b/cmd/cli/cmd/typeinstance/create.go @@ -13,7 +13,6 @@ import ( "capact.io/capact/internal/cli/heredoc" "capact.io/capact/internal/cli/printer" gqllocalapi "capact.io/capact/pkg/hub/api/graphql/local" - "capact.io/capact/pkg/sdk/apis/0.0.1/types" "capact.io/capact/pkg/sdk/validation" "github.com/pkg/errors" @@ -99,26 +98,17 @@ func createTI(ctx context.Context, opts createOptions, resourcePrinter *printer. return err } - for _, ti := range out.TypeInstances { - validationResult, err := validation.ValidateTI(ctx, &validation.TypeInstanceValidation{ - Alias: ti.Alias, - Value: ti.Value, - TypeRef: types.TypeRef{ - Path: ti.TypeRef.Path, - Revision: ti.TypeRef.Revision, - }, - }, hubCli) - if err != nil { - return errors.Wrap(err, "while validating TypeInstance") - } - if validationResult.Len() > 0 { - return validationResult.ErrorOrNil() - } - } - typeInstanceToCreate = mergeCreateTypeInstances(typeInstanceToCreate, out) } + validationResult, err := validation.ValidateTypeInstancesToCreate(ctx, hubCli, typeInstanceToCreate) + if err != nil { + return errors.Wrap(err, "while validating TypeInstances") + } + if validationResult.Len() > 0 { + return validationResult.ErrorOrNil() + } + // HACK: UsesRelations are required on GQL side so at least empty array needs to be send if typeInstanceToCreate.UsesRelations == nil { typeInstanceToCreate.UsesRelations = []*gqllocalapi.TypeInstanceUsesRelationInput{} diff --git a/cmd/cli/cmd/typeinstance/edit.go b/cmd/cli/cmd/typeinstance/edit.go index 60b3a0489..f54794b9b 100644 --- a/cmd/cli/cmd/typeinstance/edit.go +++ b/cmd/cli/cmd/typeinstance/edit.go @@ -9,7 +9,6 @@ import ( "capact.io/capact/internal/cli/client" "capact.io/capact/internal/cli/config" gqllocalapi "capact.io/capact/pkg/hub/api/graphql/local" - "capact.io/capact/pkg/sdk/apis/0.0.1/types" "capact.io/capact/pkg/sdk/validation" "github.com/AlecAivazis/survey/v2" @@ -61,28 +60,12 @@ func editTI(ctx context.Context, opts editOptions, w io.Writer) error { return err } - for _, ti := range typeInstanceToUpdate { - if ti.TypeInstance == nil { - continue - } - currentTI, err := hubCli.FindTypeInstance(ctx, ti.ID) - if err != nil { - return errors.Wrapf(err, "while finding TypeInstance %s", ti.ID) - } - - validationResult, err := validation.ValidateTI(ctx, &validation.TypeInstanceValidation{ - Value: ti.TypeInstance.Value, - TypeRef: types.TypeRef{ - Path: currentTI.TypeRef.Path, - Revision: currentTI.TypeRef.Revision, - }, - }, hubCli) - if err != nil { - return errors.Wrap(err, "while validating TypeInstance") - } - if validationResult.Len() > 0 { - return validationResult.ErrorOrNil() - } + validationResult, err := validation.ValidateTypeInstanceToUpdate(ctx, hubCli, typeInstanceToUpdate) + if err != nil { + return errors.Wrap(err, "while validating TypeInstance") + } + if validationResult.Len() > 0 { + return validationResult.ErrorOrNil() } _, err = hubCli.UpdateTypeInstances(ctx, typeInstanceToUpdate) diff --git a/pkg/argo-actions/update_type_instances.go b/pkg/argo-actions/update_type_instances.go index 50d686206..ee053d36f 100644 --- a/pkg/argo-actions/update_type_instances.go +++ b/pkg/argo-actions/update_type_instances.go @@ -6,12 +6,9 @@ import ( "path" "path/filepath" - "capact.io/capact/pkg/sdk/apis/0.0.1/types" - "capact.io/capact/pkg/sdk/validation" - graphqllocal "capact.io/capact/pkg/hub/api/graphql/local" - "capact.io/capact/pkg/hub/client/local" - "capact.io/capact/pkg/hub/client/public" + hubclient "capact.io/capact/pkg/hub/client" + "capact.io/capact/pkg/sdk/validation" "github.com/pkg/errors" "go.uber.org/zap" "sigs.k8s.io/yaml" @@ -29,19 +26,17 @@ type UpdateConfig struct { // Update implements the Action interface. // It is used to update existing TypeInstances in the Local Hub. type Update struct { - log *zap.Logger - localClient *local.Client - publicClient *public.Client - cfg UpdateConfig + log *zap.Logger + client *hubclient.Client + cfg UpdateConfig } // NewUpdateAction returns a new Update instance. -func NewUpdateAction(log *zap.Logger, localClient *local.Client, publicClient *public.Client, cfg UpdateConfig) Action { +func NewUpdateAction(log *zap.Logger, client *hubclient.Client, cfg UpdateConfig) Action { return &Update{ - log: log, - localClient: localClient, - publicClient: publicClient, - cfg: cfg, + log: log, + client: client, + cfg: cfg, } } @@ -91,28 +86,12 @@ func (u *Update) Do(ctx context.Context) error { u.log.Info("Validating TypeInstances") - for _, ti := range payload { - if ti.TypeInstance == nil { - continue - } - currentTI, err := u.localClient.FindTypeInstance(ctx, ti.ID) - if err != nil { - return errors.Wrapf(err, "while finding TypeInstance %s", ti.ID) - } - - validationResult, err := validation.ValidateTI(ctx, &validation.TypeInstanceValidation{ - Value: ti.TypeInstance.Value, - TypeRef: types.TypeRef{ - Path: currentTI.TypeRef.Path, - Revision: currentTI.TypeRef.Revision, - }, - }, u.publicClient) - if err != nil { - return errors.Wrap(err, "while validating TypeInstance") - } - if validationResult.Len() > 0 { - return validationResult.ErrorOrNil() - } + validationResult, err := validation.ValidateTypeInstanceToUpdate(ctx, u.client, payload) + if err != nil { + return errors.Wrap(err, "while validating TypeInstance") + } + if validationResult.Len() > 0 { + return validationResult.ErrorOrNil() } u.log.Info("Updating TypeInstances in Hub...", zap.Int("TypeInstance count", len(payload))) @@ -142,5 +121,5 @@ func (u *Update) render(payload []graphqllocal.UpdateTypeInstancesInput, values } func (u *Update) updateTypeInstances(ctx context.Context, in []graphqllocal.UpdateTypeInstancesInput) ([]graphqllocal.TypeInstance, error) { - return u.localClient.UpdateTypeInstances(ctx, in) + return u.client.Local.UpdateTypeInstances(ctx, in) } diff --git a/pkg/argo-actions/upload_type_instances.go b/pkg/argo-actions/upload_type_instances.go index 917fc1788..cefa9ad87 100644 --- a/pkg/argo-actions/upload_type_instances.go +++ b/pkg/argo-actions/upload_type_instances.go @@ -7,11 +7,9 @@ import ( "path/filepath" graphqllocal "capact.io/capact/pkg/hub/api/graphql/local" - "capact.io/capact/pkg/sdk/apis/0.0.1/types" + hubclient "capact.io/capact/pkg/hub/client" "capact.io/capact/pkg/sdk/validation" - "capact.io/capact/pkg/hub/client/local" - "capact.io/capact/pkg/hub/client/public" "github.com/pkg/errors" "go.uber.org/zap" "sigs.k8s.io/yaml" @@ -29,19 +27,17 @@ type UploadConfig struct { // Upload implements the Action interface. // It is used to upload TypeInstances to the Local Hub. type Upload struct { - log *zap.Logger - localClient *local.Client - publicClient *public.Client - cfg UploadConfig + log *zap.Logger + client *hubclient.Client + cfg UploadConfig } // NewUploadAction returns a new Upload instance. -func NewUploadAction(log *zap.Logger, localClient *local.Client, publicClient *public.Client, cfg UploadConfig) Action { +func NewUploadAction(log *zap.Logger, client *hubclient.Client, cfg UploadConfig) Action { return &Upload{ - log: log, - localClient: localClient, - publicClient: publicClient, - cfg: cfg, + log: log, + client: client, + cfg: cfg, } } @@ -91,20 +87,12 @@ func (u *Upload) Do(ctx context.Context) error { u.log.Info("Validating TypeInstances") - for _, ti := range payload.TypeInstances { - validationResult, err := validation.ValidateTI(ctx, &validation.TypeInstanceValidation{ - Value: ti.Value, - TypeRef: types.TypeRef{ - Path: ti.TypeRef.Path, - Revision: ti.TypeRef.Revision, - }, - }, u.publicClient) - if err != nil { - return errors.Wrap(err, "while validating TypeInstance") - } - if validationResult.Len() > 0 { - return validationResult.ErrorOrNil() - } + validationResult, err := validation.ValidateTypeInstancesToCreate(ctx, u.client, payload) + if err != nil { + return errors.Wrap(err, "while validating TypeInstances") + } + if validationResult.Len() > 0 { + return validationResult.ErrorOrNil() } u.log.Info("Uploading TypeInstances to Hub...", zap.Int("TypeInstance count", len(payload.TypeInstances))) @@ -136,5 +124,5 @@ func (u *Upload) render(payload *graphqllocal.CreateTypeInstancesInput, values m } func (u *Upload) uploadTypeInstances(ctx context.Context, in *graphqllocal.CreateTypeInstancesInput) ([]graphqllocal.CreateTypeInstanceOutput, error) { - return u.localClient.CreateTypeInstances(ctx, in) + return u.client.Local.CreateTypeInstances(ctx, in) } diff --git a/pkg/sdk/validation/typeinstance.go b/pkg/sdk/validation/typeinstance.go index 4f0dce51a..7eec6ff5a 100644 --- a/pkg/sdk/validation/typeinstance.go +++ b/pkg/sdk/validation/typeinstance.go @@ -3,84 +3,141 @@ package validation import ( "context" "encoding/json" - "strings" + "fmt" + graphqllocal "capact.io/capact/pkg/hub/api/graphql/local" "capact.io/capact/pkg/sdk/apis/0.0.1/types" "github.com/pkg/errors" "github.com/xeipuuv/gojsonschema" ) -// TypeInstanceValidation gather the necessary data to validate TypeInstance. -type TypeInstanceValidation struct { - Alias *string - TypeRef types.TypeRef - Value interface{} +type typeInstanceData struct { + alias *string + id string + value interface{} + typeRefWithRevision string } -// ValidateTI is responsible for validating the TypeInstance. -func ValidateTI(ctx context.Context, ti *TypeInstanceValidation, hub HubClient) (Result, error) { - if ti == nil { - return Result{}, nil +// TypeInstanceValidationHubClient defines Hub methods needed for validation of TypeInstances. +type TypeInstanceValidationHubClient interface { + HubClient + FindTypeInstancesTypeRef(ctx context.Context, ids []string) (map[string]graphqllocal.TypeInstanceTypeReference, error) +} + +// ValidateTypeInstancesToCreate is responsible for validating TypeInstance which do not exist and will be created. +func ValidateTypeInstancesToCreate(ctx context.Context, client TypeInstanceValidationHubClient, typeInstance *graphqllocal.CreateTypeInstancesInput) (Result, error) { + var typeInstanceCollection []typeInstanceData + typeRefCollection := TypeRefCollection{} + + for _, ti := range typeInstance.TypeInstances { + if ti == nil { + continue + } + typeRef := types.TypeRef{ + Path: ti.TypeRef.Path, + Revision: ti.TypeRef.Revision, + } + name := getManifestPathWithRevision(ti.TypeRef.Path, ti.TypeRef.Revision) + typeRefCollection[name] = TypeRef{ + TypeRef: typeRef, + } + typeInstanceCollection = append(typeInstanceCollection, typeInstanceData{ + alias: ti.Alias, + value: ti.Value, + typeRefWithRevision: name, + }) } - if _, ok := ti.Value.(map[string]interface{}); !ok { - return Result{}, nil + schemasCollection, err := ResolveTypeRefsToJSONSchemas(ctx, client, typeRefCollection) + if err != nil { + return nil, errors.Wrapf(err, "while resolving TypeRefs to JSON Schemas") } + return validateTypeInstances(schemasCollection, typeInstanceCollection) +} - resultBldr := NewResultBuilder("TypeInstance value") +// ValidateTypeInstanceToUpdate is responsible for validating TypeInstance which exists and will be updated. +func ValidateTypeInstanceToUpdate(ctx context.Context, client TypeInstanceValidationHubClient, typeInstanceToUpdate []graphqllocal.UpdateTypeInstancesInput) (Result, error) { + var typeInstanceIds []string + idToTypeNameMap := map[string]string{} + for _, ti := range typeInstanceToUpdate { + typeInstanceIds = append(typeInstanceIds, ti.ID) + } - typeName := getTypeNameFromPath(ti.TypeRef.Path) - typeRevision, err := ResolveTypeRefsToJSONSchemas(ctx, hub, TypeRefCollection{ - typeName: TypeRef{ - TypeRef: ti.TypeRef, - }, - }) + typeInstancesTypeRef, err := client.FindTypeInstancesTypeRef(ctx, typeInstanceIds) if err != nil { - return Result{}, errors.Wrap(err, "while resolving TypeRefs to JSON Schemas") + return nil, errors.Wrapf(err, "while finding TypeInstance Type reference") } - valuesJSON, err := convertTypeInstanceValueToJSONBytes(ti.Value) - if err != nil { - return Result{}, errors.Wrap(err, "while converting TypeInstance value to JSON bytes") + typeRefCollection := TypeRefCollection{} + for id, typeReference := range typeInstancesTypeRef { + name := getManifestPathWithRevision(typeReference.Path, typeReference.Revision) + typeRefCollection[name] = TypeRef{ + TypeRef: types.TypeRef{ + Path: typeReference.Path, + Revision: typeReference.Revision, + }, + } + idToTypeNameMap[id] = name } - schemaLoader := gojsonschema.NewStringLoader(typeRevision[typeName].Value) - dataLoader := gojsonschema.NewBytesLoader(valuesJSON) + var typeInstanceCollection []typeInstanceData + for _, ti := range typeInstanceToUpdate { + if ti.TypeInstance == nil { + continue + } + typeInstanceCollection = append(typeInstanceCollection, typeInstanceData{ + id: ti.ID, + value: ti.TypeInstance.Value, + typeRefWithRevision: idToTypeNameMap[ti.ID], + }) + } - result, err := gojsonschema.Validate(schemaLoader, dataLoader) + schemasCollection, err := ResolveTypeRefsToJSONSchemas(ctx, client, typeRefCollection) if err != nil { - return nil, err - } - if !result.Valid() { - for _, err := range result.Errors() { - name := "" - if ti.Alias != nil { - name = *ti.Alias - } - resultBldr.ReportIssue(name, err.String()) - } + return nil, errors.Wrapf(err, "while resolving TypeRefs to JSON Schemas") } - return resultBldr.Result(), nil + return validateTypeInstances(schemasCollection, typeInstanceCollection) } -func convertTypeInstanceValueToJSONBytes(values interface{}) ([]byte, error) { - parameters := make(map[string]json.RawMessage) - valueMap := values.(map[string]interface{}) +func validateTypeInstances(schemaCollection SchemaCollection, typeInstanceCollection []typeInstanceData) (Result, error) { + resultBldr := NewResultBuilder("Validation TypeInstances") - for name := range valueMap { - value := valueMap[name] - valueData, err := json.Marshal(&value) + for _, ti := range typeInstanceCollection { + if _, ok := ti.value.(map[string]interface{}); !ok { + return Result{}, errors.New("could not create map from TypeInstance Value") + } + valuesJSON, err := json.Marshal(ti.value) if err != nil { - return nil, errors.Wrapf(err, "while marshaling %s parameter to JSON", name) + return Result{}, errors.Wrap(err, "while converting TypeInstance value to JSON bytes") + } + if _, ok := schemaCollection[ti.typeRefWithRevision]; !ok { + return Result{}, fmt.Errorf("could not find Schema for type %s", ti.typeRefWithRevision) } - parameters[name] = valueData + schemaLoader := gojsonschema.NewStringLoader(schemaCollection[ti.typeRefWithRevision].Value) + dataLoader := gojsonschema.NewBytesLoader(valuesJSON) + + result, err := gojsonschema.Validate(schemaLoader, dataLoader) + if err != nil { + return nil, errors.Wrap(err, "while validating JSON schema for TypeInstance") + } + if !result.Valid() { + for _, err := range result.Errors() { + msg := "" + if ti.alias != nil { + msg = fmt.Sprintf("TypeInstance with alias %s", *ti.alias) + } else if ti.id != "" { + msg = fmt.Sprintf("TypeInstance with id %s", ti.id) + } + resultBldr.ReportIssue(msg, err.String()) + } + } } - return json.Marshal(parameters) + + return resultBldr.Result(), nil } -func getTypeNameFromPath(path string) string { - parts := strings.Split(path, ".") - return parts[len(parts)-1] +func getManifestPathWithRevision(path string, revision string) string { + return path + ":" + revision } diff --git a/pkg/sdk/validation/typeinstance_test.go b/pkg/sdk/validation/typeinstance_test.go index 93af6e2f2..4a852b13a 100644 --- a/pkg/sdk/validation/typeinstance_test.go +++ b/pkg/sdk/validation/typeinstance_test.go @@ -1,59 +1,69 @@ -package validation_test +package validation import ( - "context" "fmt" "testing" - gqlpublicapi "capact.io/capact/pkg/hub/api/graphql/public" - "capact.io/capact/pkg/sdk/apis/0.0.1/types" - "capact.io/capact/pkg/sdk/validation" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestValidateTI(t *testing.T) { +func TestValidateTypeInstances(t *testing.T) { tests := map[string]struct { - types []*gqlpublicapi.Type - typeInstance validation.TypeInstanceValidation - expError error + schemaCollection SchemaCollection + typeInstanceCollection []typeInstanceData + expError error }{ "When TypeInstance values do not contain the required property": { - types: []*gqlpublicapi.Type{validation.AWSCredsTypeRevFixture()}, - typeInstance: validation.TypeInstanceValidation{ - TypeRef: types.TypeRef{ - Path: "cap.type.aws.auth.creds", - Revision: "0.1.0", + schemaCollection: SchemaCollection{ + "cap.type.aws.auth.creds:0.1.0": { + Value: fmt.Sprintf("%v", AWSCredsTypeRevFixture().Revisions[0].Spec.JSONSchema), + Required: false, }, - Value: map[string]interface{}{ - "test1": "test", - "test2": "test", + }, + typeInstanceCollection: []typeInstanceData{ + { + typeRefWithRevision: "cap.type.aws.auth.creds:0.1.0", + value: map[string]interface{}{ + "test1": "test", + "test2": "test", + }, + alias: pointerToAlias("aws-creds"), }, }, - expError: fmt.Errorf("%s", "- TypeInstance value \"\":\n * (root): key is required"), + expError: fmt.Errorf("%s", "- Validation TypeInstances \"TypeInstance with alias aws-creds\":\n * (root): key is required"), }, "When TypeInstance value does not meet Type property constraints": { - types: []*gqlpublicapi.Type{validation.AWSElasticsearchTypeRevFixture()}, - typeInstance: validation.TypeInstanceValidation{ - TypeRef: types.TypeRef{ - Path: "cap.type.aws.elasticsearch.install-input", - Revision: "0.1.0", + schemaCollection: SchemaCollection{ + "cap.type.aws.elasticsearch.install-input:0.1.0": { + Value: fmt.Sprintf("%v", AWSElasticsearchTypeRevFixture().Revisions[0].Spec.JSONSchema), + Required: false, }, - Value: map[string]interface{}{ - "replicas": 5, + }, + typeInstanceCollection: []typeInstanceData{ + { + typeRefWithRevision: "cap.type.aws.elasticsearch.install-input:0.1.0", + value: map[string]interface{}{ + "replicas": 5, + }, + id: "5605af48-c34f-4bdc-b2d8-53c679bdfa5a", }, }, - expError: fmt.Errorf("%s", "- TypeInstance value \"\":\n * replicas: Invalid type. Expected: string, given: integer"), + expError: fmt.Errorf("%s", "- Validation TypeInstances \"TypeInstance with id 5605af48-c34f-4bdc-b2d8-53c679bdfa5a\":\n * replicas: Invalid type. Expected: string, given: integer"), }, "When TypeInstance contain the required property": { - types: []*gqlpublicapi.Type{validation.AWSCredsTypeRevFixture()}, - typeInstance: validation.TypeInstanceValidation{ - TypeRef: types.TypeRef{ - Path: "cap.type.aws.auth.creds", - Revision: "0.1.0", + schemaCollection: SchemaCollection{ + "cap.type.aws.auth.creds:0.1.0": { + Value: fmt.Sprintf("%v", AWSCredsTypeRevFixture().Revisions[0].Spec.JSONSchema), + Required: false, }, - Value: map[string]interface{}{ - "key": "aaa", + }, + typeInstanceCollection: []typeInstanceData{ + { + typeRefWithRevision: "cap.type.aws.auth.creds:0.1.0", + value: map[string]interface{}{ + "key": "aaa", + }, }, }, expError: nil, @@ -62,13 +72,8 @@ func TestValidateTI(t *testing.T) { for tn, tc := range tests { t.Run(tn, func(t *testing.T) { - // given - hubCli := validation.FakeHubCli{ - Types: tc.types, - } - // when - validationResults, err := validation.ValidateTI(context.Background(), &tc.typeInstance, &hubCli) + validationResults, err := validateTypeInstances(tc.schemaCollection, tc.typeInstanceCollection) // then require.NoError(t, err) @@ -76,3 +81,7 @@ func TestValidateTI(t *testing.T) { }) } } + +func pointerToAlias(alias string) *string { + return &alias +} diff --git a/test/e2e/action_test.go b/test/e2e/action_test.go index 0ad88d8d9..52b76f184 100644 --- a/test/e2e/action_test.go +++ b/test/e2e/action_test.go @@ -1,3 +1,4 @@ +//go:build integration // +build integration package e2e