diff --git a/cmd/argo-actions/main.go b/cmd/argo-actions/main.go index d68a80bab..097a6f34c 100644 --- a/cmd/argo-actions/main.go +++ b/cmd/argo-actions/main.go @@ -53,7 +53,7 @@ func main() { case argoactions.UpdateAction: log := logger.With(zap.String("Action", argoactions.UpdateAction)) - action = argoactions.NewUpdateAction(log, localClient, cfg.UpdateConfig) + action = argoactions.NewUpdateAction(log, localClient, publicClient, 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 1d96f5f95..40ce9219a 100644 --- a/cmd/cli/cmd/typeinstance/create.go +++ b/cmd/cli/cmd/typeinstance/create.go @@ -13,8 +13,8 @@ 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/validation/manifest" + "capact.io/capact/pkg/sdk/apis/0.0.1/types" + "capact.io/capact/pkg/sdk/validation" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -100,12 +100,19 @@ func createTI(ctx context.Context, opts createOptions, resourcePrinter *printer. } for _, ti := range out.TypeInstances { - validationResult, err := manifest.ValidateTI(ctx, ti, hubCli) + 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 err + return errors.Wrap(err, "while validating TypeInstance") } - if len(validationResult.Errors) > 0 { - return fmt.Errorf("%s", validationResult.Errors) + if validationResult.Len() > 0 { + return validationResult.ErrorOrNil() } } diff --git a/cmd/cli/cmd/typeinstance/edit.go b/cmd/cli/cmd/typeinstance/edit.go index 28d40efb1..60b3a0489 100644 --- a/cmd/cli/cmd/typeinstance/edit.go +++ b/cmd/cli/cmd/typeinstance/edit.go @@ -9,10 +9,13 @@ 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" "github.com/MakeNowJust/heredoc" "github.com/fatih/color" + "github.com/pkg/errors" "github.com/spf13/cobra" "sigs.k8s.io/yaml" ) @@ -58,6 +61,30 @@ 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() + } + } + _, err = hubCli.UpdateTypeInstances(ctx, typeInstanceToUpdate) if err != nil { return err diff --git a/internal/cli/client/hub.go b/internal/cli/client/hub.go index 7a3ee8222..769c54f34 100644 --- a/internal/cli/client/hub.go +++ b/internal/cli/client/hub.go @@ -25,7 +25,6 @@ type Hub interface { ListInterfaces(ctx context.Context, opts ...public.InterfaceOption) ([]*gqlpublicapi.Interface, error) ListTypeInstances(ctx context.Context, filter *gqllocalapi.TypeInstanceFilter, opts ...local.TypeInstancesOption) ([]gqllocalapi.TypeInstance, error) ListImplementationRevisions(ctx context.Context, opts ...public.ListImplementationRevisionsOption) ([]*gqlpublicapi.ImplementationRevision, error) - FindTypeRevision(ctx context.Context, ref gqlpublicapi.TypeReference, opts ...public.TypeRevisionOption) (*gqlpublicapi.TypeRevision, error) FindTypeInstance(ctx context.Context, id string, opts ...local.TypeInstancesOption) (*gqllocalapi.TypeInstance, error) CreateTypeInstance(ctx context.Context, in *gqllocalapi.CreateTypeInstanceInput, opts ...local.TypeInstancesOption) (*gqllocalapi.TypeInstance, error) CreateTypeInstances(ctx context.Context, in *gqllocalapi.CreateTypeInstancesInput) ([]gqllocalapi.CreateTypeInstanceOutput, error) @@ -34,9 +33,6 @@ type Hub interface { FindInterfaceRevision(ctx context.Context, ref gqlpublicapi.InterfaceReference, opts ...public.InterfaceRevisionOption) (*gqlpublicapi.InterfaceRevision, error) FindTypeInstancesTypeRef(ctx context.Context, ids []string) (map[string]gqllocalapi.TypeInstanceTypeReference, error) CheckManifestRevisionsExist(ctx context.Context, manifestRefs []gqlpublicapi.ManifestReference) (map[gqlpublicapi.ManifestReference]bool, error) - // not implemented - GetInterfaceLatestRevisionString(ctx context.Context, ref gqlpublicapi.InterfaceReference) (string, error) - ListImplementationRevisionsForInterface(ctx context.Context, ref gqlpublicapi.InterfaceReference, opts ...public.ListImplementationRevisionsForInterfaceOption) ([]gqlpublicapi.ImplementationRevision, error) } // NewHub returns client for Capact Hub configured with saved credentials for a given server URL. diff --git a/internal/cli/validate/validate.go b/internal/cli/validate/validate.go index 148c243be..b49f3a849 100644 --- a/internal/cli/validate/validate.go +++ b/internal/cli/validate/validate.go @@ -59,10 +59,6 @@ func (r *ValidationResult) Error() string { errMsgs = append(errMsgs, err.Error()) } - if r.Path == "" { - return fmt.Sprintf("\n * %s\n", strings.Join(errMsgs, "\n * ")) - } - return fmt.Sprintf("%q:\n * %s\n", r.Path, strings.Join(errMsgs, "\n * ")) } diff --git a/pkg/argo-actions/update_type_instances.go b/pkg/argo-actions/update_type_instances.go index faa30f87e..50d686206 100644 --- a/pkg/argo-actions/update_type_instances.go +++ b/pkg/argo-actions/update_type_instances.go @@ -6,8 +6,12 @@ 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" "github.com/pkg/errors" "go.uber.org/zap" "sigs.k8s.io/yaml" @@ -25,17 +29,19 @@ 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 - client *local.Client - cfg UpdateConfig + log *zap.Logger + localClient *local.Client + publicClient *public.Client + cfg UpdateConfig } // NewUpdateAction returns a new Update instance. -func NewUpdateAction(log *zap.Logger, client *local.Client, cfg UpdateConfig) Action { +func NewUpdateAction(log *zap.Logger, localClient *local.Client, publicClient *public.Client, cfg UpdateConfig) Action { return &Update{ - log: log, - client: client, - cfg: cfg, + log: log, + localClient: localClient, + publicClient: publicClient, + cfg: cfg, } } @@ -83,6 +89,32 @@ func (u *Update) Do(ctx context.Context) error { return errors.Wrap(err, "while rendering UpdateTypeInstancesInput") } + 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() + } + } + u.log.Info("Updating TypeInstances in Hub...", zap.Int("TypeInstance count", len(payload))) uploadOutput, err := u.updateTypeInstances(ctx, payload) @@ -110,5 +142,5 @@ func (u *Update) render(payload []graphqllocal.UpdateTypeInstancesInput, values } func (u *Update) updateTypeInstances(ctx context.Context, in []graphqllocal.UpdateTypeInstancesInput) ([]graphqllocal.TypeInstance, error) { - return u.client.UpdateTypeInstances(ctx, in) + return u.localClient.UpdateTypeInstances(ctx, in) } diff --git a/pkg/argo-actions/upload_type_instances.go b/pkg/argo-actions/upload_type_instances.go index 1289a6e47..917fc1788 100644 --- a/pkg/argo-actions/upload_type_instances.go +++ b/pkg/argo-actions/upload_type_instances.go @@ -7,7 +7,8 @@ import ( "path/filepath" graphqllocal "capact.io/capact/pkg/hub/api/graphql/local" - "capact.io/capact/pkg/sdk/validation/manifest" + "capact.io/capact/pkg/sdk/apis/0.0.1/types" + "capact.io/capact/pkg/sdk/validation" "capact.io/capact/pkg/hub/client/local" "capact.io/capact/pkg/hub/client/public" @@ -88,16 +89,21 @@ func (u *Upload) Do(ctx context.Context) error { return errors.Wrap(err, "while rendering CreateTypeInstancesInput") } + u.log.Info("Validating TypeInstances") + for _, ti := range payload.TypeInstances { - u.log.Info(fmt.Sprintf("Validating TypeInstance... %s", *ti.Alias)) - u.log.Info(fmt.Sprintf("ti %+v", *ti)) - u.log.Info(fmt.Sprintf("TypeRef %+v", *ti.TypeRef)) - validationResult, err := manifest.ValidateTI(ctx, ti, u.publicClient) + 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 err + return errors.Wrap(err, "while validating TypeInstance") } - if len(validationResult.Errors) > 0 { - return fmt.Errorf("%s", validationResult.Errors) + if validationResult.Len() > 0 { + return validationResult.ErrorOrNil() } } diff --git a/pkg/hub/client/client.go b/pkg/hub/client/client.go index b8138ba64..e2813f42d 100644 --- a/pkg/hub/client/client.go +++ b/pkg/hub/client/client.go @@ -35,7 +35,6 @@ type Local interface { // Public interface aggregates methods to interact with Capact Public Hub. type Public interface { ListTypes(ctx context.Context, opts ...public.TypeOption) ([]*hubpublicgraphql.Type, error) - FindTypeRevision(ctx context.Context, ref hubpublicgraphql.TypeReference, opts ...public.TypeRevisionOption) (*hubpublicgraphql.TypeRevision, error) GetInterfaceLatestRevisionString(ctx context.Context, ref hubpublicgraphql.InterfaceReference) (string, error) FindInterfaceRevision(ctx context.Context, ref hubpublicgraphql.InterfaceReference, opts ...public.InterfaceRevisionOption) (*hubpublicgraphql.InterfaceRevision, error) ListImplementationRevisionsForInterface(ctx context.Context, ref hubpublicgraphql.InterfaceReference, opts ...public.ListImplementationRevisionsForInterfaceOption) ([]hubpublicgraphql.ImplementationRevision, error) diff --git a/pkg/hub/client/public/client.go b/pkg/hub/client/public/client.go index b597a1b8d..6c98b5bdf 100644 --- a/pkg/hub/client/public/client.go +++ b/pkg/hub/client/public/client.go @@ -101,36 +101,6 @@ func (c *Client) ListTypes(ctx context.Context, opts ...TypeOption) ([]*gqlpubli return resp.Types, nil } -func (c *Client) FindTypeRevision(ctx context.Context, ref gqlpublicapi.TypeReference, opts ...TypeRevisionOption) (*gqlpublicapi.TypeRevision, error) { - findOpts := &TypeRevisionOptions{} - findOpts.Apply(opts...) - - query, params := c.typeQueryForRef(findOpts.fields, ref) - req := graphql.NewRequest(fmt.Sprintf(`query FindTypeRevision($typePath: NodePath!, %s) { - type(path: $typePath) { - %s - } - }`, params.Query(), query)) - - req.Var("typePath", ref.Path) - params.PopulateVars(req) - - var resp struct { - Type struct { - Revision *gqlpublicapi.TypeRevision `json:"rev"` - } `json:"type"` - } - err := retry.Do(func() error { - return c.client.Run(ctx, req, &resp) - }, retry.Attempts(retryAttempts)) - - if err != nil { - return nil, errors.Wrap(err, "while executing query to fetch Hub Type Revision") - } - - return resp.Type.Revision, nil -} - // ListInterfaces returns all Interfaces. By default, only root fields are populated. Use options to add // latestRevision fields or apply additional filtering. func (c *Client) ListInterfaces(ctx context.Context, opts ...InterfaceOption) ([]*gqlpublicapi.Interface, error) { @@ -396,31 +366,3 @@ func (c *Client) specificInterfaceRevision(fields string, rev string) (string, A "$interfaceRev: Version!": rev, } } - -func (c *Client) typeQueryForRef(fields string, ref gqlpublicapi.TypeReference) (string, Args) { - if ref.Revision == "" { - return c.latestTypeRevision(fields) - } - - return c.specificTypeRevision(fields, ref.Revision) -} - -func (c *Client) latestTypeRevision(fields string) (string, Args) { - latestRevision := fmt.Sprintf(` - rev: latestRevision { - %s - }`, fields) - - return latestRevision, Args{} -} - -func (c *Client) specificTypeRevision(fields string, rev string) (string, Args) { - specificRevision := fmt.Sprintf(` - rev: revision(revision: $typeRev) { - %s - }`, fields) - - return specificRevision, Args{ - "$typeRev: Version!": rev, - } -} diff --git a/pkg/sdk/renderer/argo/dedicated_renderer.go b/pkg/sdk/renderer/argo/dedicated_renderer.go index 8a44d3d69..d89669ec0 100644 --- a/pkg/sdk/renderer/argo/dedicated_renderer.go +++ b/pkg/sdk/renderer/argo/dedicated_renderer.go @@ -75,7 +75,11 @@ func newDedicatedRenderer(maxDepth int, policyEnforcedCli PolicyEnforcedHubClien return r } -func (r *dedicatedRenderer) WrapEntrypointWithRootStep(workflow *Workflow) (*Workflow, *WorkflowStep) { +func (r *dedicatedRenderer) WrapEntrypointWithRootStep(workflow *Workflow) (*Workflow, *WorkflowStep, error) { + if workflow == nil || workflow.WorkflowSpec == nil || workflow.Entrypoint == "" { + return nil, nil, errors.New("workflow and its entrypoint cannot be empty") + } + r.entrypointStep = &WorkflowStep{ WorkflowStep: &wfv1.WorkflowStep{ Name: "start-entrypoint", @@ -98,7 +102,7 @@ func (r *dedicatedRenderer) WrapEntrypointWithRootStep(workflow *Workflow) (*Wor workflow.Entrypoint = r.rootTemplate.Name workflow.Templates = append(workflow.Templates, r.rootTemplate) - return workflow, r.entrypointStep + return workflow, r.entrypointStep, nil } func (r *dedicatedRenderer) AppendAdditionalInputTypeInstances(typeInstances []types.InputTypeInstanceRef) { @@ -396,7 +400,9 @@ func (r *dedicatedRenderer) UnmarshalWorkflowFromImplementation(prefix string, i if err != nil { return nil, nil, errors.Wrap(err, "while unmarshaling Argo Workflow from OCF Implementation") } - + if workflow == nil || workflow.WorkflowSpec == nil || workflow.Entrypoint == "" { + return nil, nil, errors.New("workflow and its entrypoint cannot be empty") + } artifactsNameMapping := map[string]string{} for i := range workflow.Templates { diff --git a/pkg/sdk/renderer/argo/helpers.go b/pkg/sdk/renderer/argo/helpers.go index e8c8718b7..8298f8ee0 100644 --- a/pkg/sdk/renderer/argo/helpers.go +++ b/pkg/sdk/renderer/argo/helpers.go @@ -19,7 +19,8 @@ func interfaceRefToHub(in types.InterfaceRef) hubpublicgraphql.InterfaceReferenc } } -func getEntrypointWorkflowIndex(w *Workflow) (int, error) { +// GetEntrypointWorkflowIndex returns workflow entrypoint index +func GetEntrypointWorkflowIndex(w *Workflow) (int, error) { if w == nil { return 0, NewWorkflowNilError() } diff --git a/pkg/sdk/renderer/argo/renderer.go b/pkg/sdk/renderer/argo/renderer.go index 5f4f45e6b..831728fdd 100644 --- a/pkg/sdk/renderer/argo/renderer.go +++ b/pkg/sdk/renderer/argo/renderer.go @@ -116,7 +116,10 @@ func (r *Renderer) Render(ctx context.Context, input *RenderInput) (*RenderOutpu } // 3.1 Add our own root step and replace entrypoint - rootWorkflow, entrypointStep := dedicatedRenderer.WrapEntrypointWithRootStep(rootWorkflow) + rootWorkflow, entrypointStep, err := dedicatedRenderer.WrapEntrypointWithRootStep(rootWorkflow) + if err != nil { + return nil, errors.Wrap(err, "while wrapping entrypoint with root step") + } // 4. Add user input if err := dedicatedRenderer.AddUserInputSecretRefIfProvided(rootWorkflow); err != nil { diff --git a/pkg/sdk/renderer/argo/typeinstance_handler.go b/pkg/sdk/renderer/argo/typeinstance_handler.go index 2978067b3..43644d8ad 100644 --- a/pkg/sdk/renderer/argo/typeinstance_handler.go +++ b/pkg/sdk/renderer/argo/typeinstance_handler.go @@ -38,7 +38,7 @@ func (r *TypeInstanceHandler) AddInputTypeInstances(rootWorkflow *Workflow, inst return nil } - idx, err := getEntrypointWorkflowIndex(rootWorkflow) + idx, err := GetEntrypointWorkflowIndex(rootWorkflow) if err != nil { return err } @@ -204,7 +204,7 @@ func (r *TypeInstanceHandler) AddUploadTypeInstancesStep(rootWorkflow *Workflow, }, } - idx, err := getEntrypointWorkflowIndex(rootWorkflow) + idx, err := GetEntrypointWorkflowIndex(rootWorkflow) if err != nil { return err } @@ -295,7 +295,7 @@ func (r *TypeInstanceHandler) AddUpdateTypeInstancesStep(rootWorkflow *Workflow, }, } - idx, err := getEntrypointWorkflowIndex(rootWorkflow) + idx, err := GetEntrypointWorkflowIndex(rootWorkflow) if err != nil { return err } diff --git a/pkg/sdk/validation/manifest/helpers_test.go b/pkg/sdk/validation/manifest/helpers_test.go index 2c8967ca6..362cd08a3 100644 --- a/pkg/sdk/validation/manifest/helpers_test.go +++ b/pkg/sdk/validation/manifest/helpers_test.go @@ -13,8 +13,9 @@ import ( ) type fakeHub struct { - checkManifestsFn func(ctx context.Context, manifestRefs []gqlpublicapi.ManifestReference) (map[gqlpublicapi.ManifestReference]bool, error) - knownTypes []*gqlpublicapi.Type + checkManifestsFn func(ctx context.Context, manifestRefs []gqlpublicapi.ManifestReference) (map[gqlpublicapi.ManifestReference]bool, error) + knownTypes []*gqlpublicapi.Type + interfaceRevision *gqlpublicapi.InterfaceRevision } func (h *fakeHub) ListTypes(_ context.Context, opts ...public.TypeOption) ([]*gqlpublicapi.Type, error) { @@ -47,6 +48,10 @@ func (h *fakeHub) CheckManifestRevisionsExist(ctx context.Context, manifestRefs return h.checkManifestsFn(ctx, manifestRefs) } +func (h *fakeHub) FindInterfaceRevision(_ context.Context, _ gqlpublicapi.InterfaceReference, _ ...public.InterfaceRevisionOption) (*gqlpublicapi.InterfaceRevision, error) { + return h.interfaceRevision, nil +} + func fixHub(t *testing.T, knownListTypes []*gqlpublicapi.Type, manifests map[gqlpublicapi.ManifestReference]bool, err error) *fakeHub { hub := fixHubForManifestsExistence(t, manifests, err) hub.knownTypes = knownListTypes @@ -69,6 +74,7 @@ func fixHubForManifestsExistence(t *testing.T, result map[gqlpublicapi.ManifestR return result, err }, + interfaceRevision: &gqlpublicapi.InterfaceRevision{}, } return hub } diff --git a/pkg/sdk/validation/manifest/json_remote_helper.go b/pkg/sdk/validation/manifest/json_remote_helper.go index 33b2302a0..72edc8c2a 100644 --- a/pkg/sdk/validation/manifest/json_remote_helper.go +++ b/pkg/sdk/validation/manifest/json_remote_helper.go @@ -13,6 +13,7 @@ import ( // Hub is an interface for Hub GraphQL client methods needed for the remote validation. type Hub interface { CheckManifestRevisionsExist(ctx context.Context, manifestRefs []gqlpublicapi.ManifestReference) (map[gqlpublicapi.ManifestReference]bool, error) + FindInterfaceRevision(ctx context.Context, ref gqlpublicapi.InterfaceReference, opts ...public.InterfaceRevisionOption) (*gqlpublicapi.InterfaceRevision, error) ListTypes(ctx context.Context, opts ...public.TypeOption) ([]*gqlpublicapi.Type, error) } diff --git a/pkg/sdk/validation/manifest/json_remote_implementation.go b/pkg/sdk/validation/manifest/json_remote_implementation.go index f8e03a953..5d5a62357 100644 --- a/pkg/sdk/validation/manifest/json_remote_implementation.go +++ b/pkg/sdk/validation/manifest/json_remote_implementation.go @@ -11,6 +11,10 @@ import ( gqlpublicapi "capact.io/capact/pkg/hub/api/graphql/public" "capact.io/capact/pkg/hub/client/public" "capact.io/capact/pkg/sdk/apis/0.0.1/types" + wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" + + "capact.io/capact/pkg/sdk/renderer/argo" + "k8s.io/utils/strings/slices" "github.com/dustin/go-humanize/english" "github.com/pkg/errors" @@ -47,7 +51,11 @@ func (v *RemoteImplementationValidator) Do(ctx context.Context, _ types.Manifest if err != nil { return ValidationResult{}, errors.Wrap(err, "while unmarshalling JSON into Implementation type") } - validateFns := []validateFn{v.checkManifestRevisionsExist, v.checkRequiresParentNodes} + validateFns := []validateFn{ + v.checkManifestRevisionsExist, + v.checkRequiresParentNodes, + v.validateInputArtifactsNames, + } for _, fn := range validateFns { validationResults, err := fn(ctx, entity) @@ -115,10 +123,107 @@ func (v *RemoteImplementationValidator) checkManifestRevisionsExist(ctx context. }) } } - return checkManifestRevisionsExist(ctx, v.hub, manifestRefsToCheck) } +func (v *RemoteImplementationValidator) validateInputArtifactsNames(ctx context.Context, entity types.Implementation) (ValidationResult, error) { + var validationErrs []error + var interfacesInputNames []string + var implAdditionalInput []string + var workflowArtifacts []wfv1.Artifact + + //1. get interface input names + for _, implementsItem := range entity.Spec.Implements { + interfaceInput, err := v.fetchInterfaceInput(ctx, gqlpublicapi.InterfaceReference{ + Path: implementsItem.Path, + Revision: implementsItem.Revision, + }, v.hub) + if err != nil { + return ValidationResult{}, errors.Wrap(err, "while fetching Interface inputs") + } + for _, inputParameter := range interfaceInput.Parameters { + interfacesInputNames = append(interfacesInputNames, inputParameter.Name) + } + for _, inputTypeInstance := range interfaceInput.TypeInstances { + interfacesInputNames = append(interfacesInputNames, inputTypeInstance.Name) + } + } + + //2. get implementation additional inputs + if entity.Spec.AdditionalInput != nil { + for name := range entity.Spec.AdditionalInput.Parameters { + implAdditionalInput = append(implAdditionalInput, name) + } + for name := range entity.Spec.AdditionalInput.TypeInstances { + implAdditionalInput = append(implAdditionalInput, name) + } + } + + //3. get inputs from entrypoint workflow template + workflow, err := v.decodeImplArgsToArgoWorkflow(entity.Spec.Action.Args) + if err != nil { + return ValidationResult{}, errors.Wrap(err, "while decoding Implementation arguments to Argo workflow") + } + if workflow != nil && workflow.WorkflowSpec != nil { + idx, err := argo.GetEntrypointWorkflowIndex(workflow) + if err != nil { + return ValidationResult{}, errors.Wrap(err, "while getting entrypoint index from workflow") + } + workflowArtifacts = workflow.Templates[idx].Inputs.Artifacts + } + + //4. verify if the inputs from Implementation and Interface match with Argo workflow artifacts + for _, artifact := range workflowArtifacts { + existsInInterface := slices.Contains(interfacesInputNames, artifact.Name) + existsInAdditionalInput := slices.Contains(implAdditionalInput, artifact.Name) + + if existsInInterface && + artifact.Optional { + validationErrs = append(validationErrs, fmt.Errorf("invalid workflow input artifact %q: it shouldn't be optional as it is defined as Interface input", artifact.Name)) + } + if existsInAdditionalInput && !artifact.Optional { + validationErrs = append(validationErrs, fmt.Errorf("invalid workflow input artifact %q: it should be optional, as it is defined as Implementation additional input", artifact.Name)) + } + if !existsInInterface && !existsInAdditionalInput { + validationErrs = append(validationErrs, fmt.Errorf("unknown workflow input artifact %q: there is no such input neither in Interface input, nor Implementation additional input", artifact.Name)) + } + } + + return ValidationResult{Errors: validationErrs}, nil +} + +func (v *RemoteImplementationValidator) fetchInterfaceInput(ctx context.Context, interfaceRef gqlpublicapi.InterfaceReference, hub Hub) (gqlpublicapi.InterfaceInput, error) { + iface, err := hub.FindInterfaceRevision(ctx, interfaceRef, public.WithInterfaceRevisionFields(public.InterfaceRevisionInputFields)) + if err != nil { + return gqlpublicapi.InterfaceInput{}, errors.Wrap(err, "while looking for Interface definition") + } + if iface == nil { + return gqlpublicapi.InterfaceInput{}, fmt.Errorf("interface %s:%s was not found in Hub", interfaceRef.Path, interfaceRef.Revision) + } + + if iface.Spec == nil || iface.Spec.Input == nil { + return gqlpublicapi.InterfaceInput{}, nil + } + + return *iface.Spec.Input, nil +} + +func (v *RemoteImplementationValidator) decodeImplArgsToArgoWorkflow(implArgs map[string]interface{}) (*argo.Workflow, error) { + var decodedImplArgs = struct { + Workflow argo.Workflow `json:"workflow"` + }{} + + b, err := json.Marshal(implArgs) + if err != nil { + return nil, errors.Wrap(err, "while marshaling Implementation arguments") + } + + if err := json.Unmarshal(b, &decodedImplArgs); err != nil { + return nil, errors.Wrap(err, "while unmarshalling Implementation arguments to Argo Workflow") + } + return &decodedImplArgs.Workflow, nil +} + func (v *RemoteImplementationValidator) checkRequiresParentNodes(ctx context.Context, entity types.Implementation) (ValidationResult, error) { parentNodeTypesToCheck := ParentNodesAssociation{} for requiresKey, reqItem := range entity.Spec.Requires { diff --git a/pkg/sdk/validation/manifest/json_remote_implementation_export_test.go b/pkg/sdk/validation/manifest/json_remote_implementation_export_test.go index 0c9815bc4..c9a315498 100644 --- a/pkg/sdk/validation/manifest/json_remote_implementation_export_test.go +++ b/pkg/sdk/validation/manifest/json_remote_implementation_export_test.go @@ -1,9 +1,18 @@ package manifest -import "context" +import ( + "context" + + "capact.io/capact/pkg/sdk/apis/0.0.1/types" +) // CheckParentNodesAssociation is just a hack to export the internal method for testing purposes. // The *_test.go files are not compiled into final binary, and as it's under _test.go it's also not accessible for other non-testing packages. func (v *RemoteImplementationValidator) CheckParentNodesAssociation(ctx context.Context, relations ParentNodesAssociation) (ValidationResult, error) { return v.checkParentNodesAssociation(ctx, relations) } + +//ValidateInputArtifactsNames exports validateInputArtifactsNames method for testing purposes. +func (v *RemoteImplementationValidator) ValidateInputArtifactsNames(ctx context.Context, entity types.Implementation) (ValidationResult, error) { + return v.validateInputArtifactsNames(ctx, entity) +} diff --git a/pkg/sdk/validation/manifest/json_remote_implementation_test.go b/pkg/sdk/validation/manifest/json_remote_implementation_test.go index d18fa5813..892776c30 100644 --- a/pkg/sdk/validation/manifest/json_remote_implementation_test.go +++ b/pkg/sdk/validation/manifest/json_remote_implementation_test.go @@ -3,15 +3,136 @@ package manifest_test import ( "context" "errors" + "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/renderer/argo" "capact.io/capact/pkg/sdk/validation/manifest" + wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func TestValidateInputArtifactsNames(t *testing.T) { + inputParametersName := "input-parameters" + additionalParameterName := "additional-parameters" + tests := map[string]struct { + ifaceInput *gqlpublicapi.InterfaceInput + implAdditionalInput *types.AdditionalInput + argoArtifacts []wfv1.Artifact + exprectedErrors []error + }{ + "When Argo input does not exist in the Interface and Implementation": { + ifaceInput: &gqlpublicapi.InterfaceInput{ + Parameters: []*gqlpublicapi.InputParameter{}, + }, + implAdditionalInput: &types.AdditionalInput{ + Parameters: map[string]types.AdditionalInputParameter{ + additionalParameterName: {}, + }, + }, + argoArtifacts: []wfv1.Artifact{ + { + Name: inputParametersName, + }, + { + Name: additionalParameterName, + Optional: true, + }, + }, + exprectedErrors: []error{fmt.Errorf("unknown workflow input artifact \"%s\": there is no such input neither in Interface input, nor Implementation additional input", inputParametersName)}, + }, + "When Argo input has optional input that does exist in Interface ": { + ifaceInput: &gqlpublicapi.InterfaceInput{ + Parameters: []*gqlpublicapi.InputParameter{ + { + Name: inputParametersName, + }, + }, + }, + implAdditionalInput: &types.AdditionalInput{}, + argoArtifacts: []wfv1.Artifact{ + { + Name: inputParametersName, + Optional: true, + }, + }, + exprectedErrors: []error{fmt.Errorf("invalid workflow input artifact \"%s\": it shouldn't be optional as it is defined as Interface input", inputParametersName)}, + }, + "When Argo input is not optional but exists in Implementation additional inputs": { + ifaceInput: &gqlpublicapi.InterfaceInput{ + Parameters: []*gqlpublicapi.InputParameter{ + { + Name: inputParametersName, + }, + }, + }, + implAdditionalInput: &types.AdditionalInput{ + Parameters: map[string]types.AdditionalInputParameter{ + additionalParameterName: {}, + }, + }, + argoArtifacts: []wfv1.Artifact{ + { + Name: additionalParameterName, + Optional: false, + }, + }, + exprectedErrors: []error{fmt.Errorf("invalid workflow input artifact \"%s\": it should be optional, as it is defined as Implementation additional input", additionalParameterName)}, + }, + "When Argo inputs are correctly set": { + ifaceInput: &gqlpublicapi.InterfaceInput{ + Parameters: []*gqlpublicapi.InputParameter{ + { + Name: inputParametersName, + }, + }, + }, + implAdditionalInput: &types.AdditionalInput{ + Parameters: map[string]types.AdditionalInputParameter{ + additionalParameterName: {}, + }, + }, + argoArtifacts: []wfv1.Artifact{ + { + Name: inputParametersName, + Optional: false, + }, + { + Name: additionalParameterName, + Optional: true, + }, + }, + exprectedErrors: nil, + }, + } + for tn, tc := range tests { + t.Run(tn, func(t *testing.T) { + // given + ctx := context.Background() + hubCli := fakeHub{ + interfaceRevision: &gqlpublicapi.InterfaceRevision{ + Spec: &gqlpublicapi.InterfaceSpec{ + Input: tc.ifaceInput, + }, + }, + } + validator := manifest.NewRemoteImplementationValidator(&hubCli) + implementation := fixImplementation(tc.argoArtifacts, tc.implAdditionalInput) + + // when + result, err := validator.ValidateInputArtifactsNames(ctx, implementation) + + // then + require.NoError(t, err) + assert.Equal(t, tc.exprectedErrors, result.Errors) + }) + } +} + func TestCheckParentNodesAssociation(t *testing.T) { tests := map[string]struct { knownTypes []*gqlpublicapi.Type @@ -121,3 +242,37 @@ func fixGQLType(path, rev, parent string) *gqlpublicapi.Type { }}, } } +func fixImplementation(inputArtifacts []wfv1.Artifact, implAdditionalInput *types.AdditionalInput) types.Implementation { + workflow := argo.Workflow{ + WorkflowSpec: &wfv1.WorkflowSpec{ + Entrypoint: "test", + }, + Templates: []*argo.Template{ + { + Template: &wfv1.Template{ + Name: "test", + Inputs: wfv1.Inputs{ + Artifacts: inputArtifacts, + }, + }, + }, + }, + } + + return types.Implementation{ + Spec: types.ImplementationSpec{ + Implements: []types.Implement{ + { + Path: "cap.interface.test.impl", + Revision: "0.1.0", + }, + }, + Action: types.Action{ + Args: map[string]interface{}{ + "workflow": workflow, + }, + }, + AdditionalInput: implAdditionalInput, + }, + } +} diff --git a/pkg/sdk/validation/manifest/typeinstance.go b/pkg/sdk/validation/manifest/typeinstance.go deleted file mode 100644 index 3ba499266..000000000 --- a/pkg/sdk/validation/manifest/typeinstance.go +++ /dev/null @@ -1,62 +0,0 @@ -package manifest - -import ( - "context" - "encoding/json" - "fmt" - - graphql "capact.io/capact/pkg/hub/api/graphql/public" - "k8s.io/utils/strings/slices" - - "capact.io/capact/pkg/hub/client" - - gqllocalapi "capact.io/capact/pkg/hub/api/graphql/local" -) - -type TypeJSONSchema struct { - Properties map[string]struct { - Id string `json:"$id"` - Type string `json:"type"` - Title string `json:"title"` - } `json:"properties"` -} - -// ValidateTI is responsible for validating the TypeInstance. -func ValidateTI(ctx context.Context, ti *gqllocalapi.CreateTypeInstanceInput, cl client.Public) (ValidationResult, error) { - if ti == nil { - return ValidationResult{}, nil - } - var errors []error - - typeRevision, err := cl.FindTypeRevision(ctx, graphql.TypeReference{ - Path: ti.TypeRef.Path, - Revision: ti.TypeRef.Revision, - }) - if err != nil { - return ValidationResult{}, err - } - - var typeJSONSchema TypeJSONSchema - if err := json.Unmarshal([]byte(fmt.Sprintf("%v", typeRevision.Spec.JSONSchema)), &typeJSONSchema); err != nil { - return ValidationResult{}, err - } - - var validKeys []string - for key := range typeJSONSchema.Properties { - validKeys = append(validKeys, key) - } - - // validate the keys - mapValues := ti.Value.(map[string]interface{}) - if len(mapValues) > 0 { - for key := range mapValues { - if !slices.Contains(validKeys, key) { - errors = append(errors, fmt.Errorf("key value: %s no defined by the Type", key)) - } - } - } - - return ValidationResult{ - Errors: errors, - }, nil -} diff --git a/pkg/sdk/validation/manifest/typeinstance_test.go b/pkg/sdk/validation/manifest/typeinstance_test.go deleted file mode 100644 index ade8f43e6..000000000 --- a/pkg/sdk/validation/manifest/typeinstance_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package manifest_test - -import ( - "testing" -) - -func TestValidateTI(t *testing.T) { - //tests := map[string]struct { - // typeInstance gqllocalapi.CreateTypeInstanceInput - // typeRevision hubpublicgraphql.TypeRevision - // expErrors []error - //}{ - // "": { - // typeInstance: gqllocalapi.CreateTypeInstanceInput{ - // TypeRef: &gqllocalapi.TypeInstanceTypeReferenceInput{ - // Path: "test", - // Revision: "0.1.0", - // }, - // Value: manifest.TypeJSONSchema{ - // Properties: map[string]struct { - // Id string "json:\"$id\"" - // Type string "json:\"type\"" - // Title string "json:\"title\"" - // }{ - // "test": {}, - // "test2": {}, - // "test3": {}, - // }, - // }, - // }, - // }, - //} - // - //for tn, tc := range tests { - // t.Run(tn, func(t *testing.T) { - // //given - // hubCli := fakeHub{} - // - // // when - // validationResults, err := manifest.ValidateTI(context.Background(),&tc.typeInstance,hubCli) - // - // // then - // require.NoError(t, err) - // assert.Equal(t, tc.expErrors, validationResults.Errors) - // }) - //} -} diff --git a/pkg/sdk/validation/type_refs.go b/pkg/sdk/validation/type_refs.go index 97507cc18..e4c36cc27 100644 --- a/pkg/sdk/validation/type_refs.go +++ b/pkg/sdk/validation/type_refs.go @@ -35,7 +35,7 @@ func ResolveTypeRefsToJSONSchemas(ctx context.Context, hubCli HubClient, inTypeR PathPattern: ptr.String(typeRefsPathFilter), })) if err != nil { - return nil, errors.Wrap(err, "while fetching JSONSchemas for input TypeRefs") + return nil, errors.Wrap(err, "while fetching JSONSchemas for TypeRefs") } indexedTypes := map[string]interface{}{} diff --git a/pkg/sdk/validation/type_refs_test.go b/pkg/sdk/validation/type_refs_test.go index a8a47f670..a8649be11 100644 --- a/pkg/sdk/validation/type_refs_test.go +++ b/pkg/sdk/validation/type_refs_test.go @@ -76,7 +76,7 @@ func TestResolveTypeRefsToJSONSchemasFailures(t *testing.T) { givenTypeRefs: validation.TypeRefCollection{ "aws-creds": {}, }, - expectedErrorMsg: "while fetching JSONSchemas for input TypeRefs: hub error for testing purposes", + expectedErrorMsg: "while fetching JSONSchemas for TypeRefs: hub error for testing purposes", }, } for tn, tc := range tests { diff --git a/pkg/sdk/validation/typeinstance.go b/pkg/sdk/validation/typeinstance.go new file mode 100644 index 000000000..4f0dce51a --- /dev/null +++ b/pkg/sdk/validation/typeinstance.go @@ -0,0 +1,86 @@ +package validation + +import ( + "context" + "encoding/json" + "strings" + + "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{} +} + +// ValidateTI is responsible for validating the TypeInstance. +func ValidateTI(ctx context.Context, ti *TypeInstanceValidation, hub HubClient) (Result, error) { + if ti == nil { + return Result{}, nil + } + + if _, ok := ti.Value.(map[string]interface{}); !ok { + return Result{}, nil + } + + resultBldr := NewResultBuilder("TypeInstance value") + + typeName := getTypeNameFromPath(ti.TypeRef.Path) + typeRevision, err := ResolveTypeRefsToJSONSchemas(ctx, hub, TypeRefCollection{ + typeName: TypeRef{ + TypeRef: ti.TypeRef, + }, + }) + if err != nil { + return Result{}, errors.Wrap(err, "while resolving TypeRefs to JSON Schemas") + } + + valuesJSON, err := convertTypeInstanceValueToJSONBytes(ti.Value) + if err != nil { + return Result{}, errors.Wrap(err, "while converting TypeInstance value to JSON bytes") + } + + schemaLoader := gojsonschema.NewStringLoader(typeRevision[typeName].Value) + dataLoader := gojsonschema.NewBytesLoader(valuesJSON) + + result, err := gojsonschema.Validate(schemaLoader, dataLoader) + 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 resultBldr.Result(), nil +} + +func convertTypeInstanceValueToJSONBytes(values interface{}) ([]byte, error) { + parameters := make(map[string]json.RawMessage) + valueMap := values.(map[string]interface{}) + + for name := range valueMap { + value := valueMap[name] + valueData, err := json.Marshal(&value) + if err != nil { + return nil, errors.Wrapf(err, "while marshaling %s parameter to JSON", name) + } + + parameters[name] = valueData + } + return json.Marshal(parameters) +} + +func getTypeNameFromPath(path string) string { + parts := strings.Split(path, ".") + return parts[len(parts)-1] +} diff --git a/pkg/sdk/validation/typeinstance_test.go b/pkg/sdk/validation/typeinstance_test.go new file mode 100644 index 000000000..93af6e2f2 --- /dev/null +++ b/pkg/sdk/validation/typeinstance_test.go @@ -0,0 +1,78 @@ +package validation_test + +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) { + tests := map[string]struct { + types []*gqlpublicapi.Type + typeInstance validation.TypeInstanceValidation + 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", + }, + Value: map[string]interface{}{ + "test1": "test", + "test2": "test", + }, + }, + expError: fmt.Errorf("%s", "- TypeInstance value \"\":\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", + }, + Value: map[string]interface{}{ + "replicas": 5, + }, + }, + expError: fmt.Errorf("%s", "- TypeInstance value \"\":\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", + }, + Value: map[string]interface{}{ + "key": "aaa", + }, + }, + expError: nil, + }, + } + + 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) + + // then + require.NoError(t, err) + assert.Equal(t, tc.expError, validationResults.ErrorOrNil()) + }) + } +}