Skip to content

Commit

Permalink
feat: enable full offline lint of all resources
Browse files Browse the repository at this point in the history
Currently, the offline linting only works for Workflow because other resources (`WorkflowTemplate`, `ClusterWorkflowTemplate` and `CronWorkflow`) can depend on references to other resources.
However, this behavior is very limiting in a CI context where a user may modify both a "top-level" file and its dependencies at the same time. The dependencies are fetched from the server and the validation fails.

This PR extends the offline linter so that it uses the whole list of files passed as arguments in order to validate.
This is useful in a GitOps context where a user can pass the whole list of Argo files to the CI check.

Signed-off-by: Julien Duchesne <julien.duchesne@grafana.com>
  • Loading branch information
julienduchesne committed Nov 18, 2022
1 parent 6f76934 commit 8973fe4
Show file tree
Hide file tree
Showing 9 changed files with 272 additions and 42 deletions.
6 changes: 3 additions & 3 deletions cmd/argo/commands/client/conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ var overrides = clientcmd.ConfigOverrides{}

var (
explicitPath string
Offline bool
OfflineFiles []string
)

func AddKubectlFlagsToCmd(cmd *cobra.Command) {
Expand Down Expand Up @@ -62,7 +62,7 @@ func NewAPIClient(ctx context.Context) (context.Context, apiclient.Client) {
return GetAuthString()
},
ClientConfigSupplier: func() clientcmd.ClientConfig { return GetConfig() },
Offline: Offline,
OfflineFiles: OfflineFiles,
Context: ctx,
})
if err != nil {
Expand All @@ -72,7 +72,7 @@ func NewAPIClient(ctx context.Context) (context.Context, apiclient.Client) {
}

func Namespace() string {
if Offline {
if len(OfflineFiles) > 0 {
return ""
}
if overrides.Context.Namespace != "" {
Expand Down
6 changes: 4 additions & 2 deletions cmd/argo/commands/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ func NewLintCommand() *cobra.Command {
cat manifests.yaml | argo lint --kinds=workflows,cronworkflows -`,
Run: func(cmd *cobra.Command, args []string) {
client.Offline = offline
if offline {
client.OfflineFiles = args
}
ctx, apiClient := client.NewAPIClient(cmd.Context())
if len(args) == 0 {
cmd.HelpFunc()(cmd, args)
Expand All @@ -56,7 +58,7 @@ func NewLintCommand() *cobra.Command {
command.Flags().StringSliceVar(&lintKinds, "kinds", []string{"all"}, fmt.Sprintf("Which kinds will be linted. Can be: %s", strings.Join(allKinds, "|")))
command.Flags().StringVarP(&output, "output", "o", "pretty", "Linting results output format. One of: pretty|simple")
command.Flags().BoolVar(&strict, "strict", true, "Perform strict workflow validation")
command.Flags().BoolVar(&offline, "offline", false, "perform offline linting")
command.Flags().BoolVar(&offline, "offline", false, "perform offline linting. When using this mode, you should provide the entire list of Argo Workflows resources as arguments, in order to allow ref resolution.")

return command
}
2 changes: 1 addition & 1 deletion docs/cli/argo_lint.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ argo lint FILE... [flags]
```
-h, --help help for lint
--kinds strings Which kinds will be linted. Can be: workflows|workflowtemplates|cronworkflows|clusterworkflowtemplates (default [all])
--offline perform offline linting
--offline perform offline linting. When using this mode, you should provide the entire list of Argo Workflows resources as arguments, in order to allow ref resolution.
-o, --output string Linting results output format. One of: pretty|simple (default "pretty")
--strict Perform strict workflow validation (default true)
```
Expand Down
6 changes: 3 additions & 3 deletions pkg/apiclient/apiclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ type Opts struct {
// DEPRECATED: use `ClientConfigSupplier`
ClientConfig clientcmd.ClientConfig
ClientConfigSupplier func() clientcmd.ClientConfig
Offline bool
OfflineFiles []string
Context context.Context
}

Expand Down Expand Up @@ -62,8 +62,8 @@ func NewClient(argoServer string, authSupplier func() string, clientConfig clien

func NewClientFromOpts(opts Opts) (context.Context, Client, error) {
log.WithField("opts", opts).Debug("Client options")
if opts.Offline {
return newOfflineClient()
if len(opts.OfflineFiles) > 0 {
return newOfflineClient(opts.OfflineFiles)
}
if opts.ArgoServerOpts.URL != "" && opts.InstanceID != "" {
return nil, nil, fmt.Errorf("cannot use instance ID with Argo Server")
Expand Down
121 changes: 105 additions & 16 deletions pkg/apiclient/offline-client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,134 @@ package apiclient
import (
"context"
"fmt"
"os"

"github.com/argoproj/argo-workflows/v3/pkg/apiclient/clusterworkflowtemplate"
"github.com/argoproj/argo-workflows/v3/pkg/apiclient/cronworkflow"
infopkg "github.com/argoproj/argo-workflows/v3/pkg/apiclient/info"
workflowpkg "github.com/argoproj/argo-workflows/v3/pkg/apiclient/workflow"
workflowarchivepkg "github.com/argoproj/argo-workflows/v3/pkg/apiclient/workflowarchive"
"github.com/argoproj/argo-workflows/v3/pkg/apiclient/workflowtemplate"
"github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1"
wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1"
"github.com/argoproj/argo-workflows/v3/workflow/templateresolution"

"sigs.k8s.io/yaml"
)

type offlineClient struct{}
type offlineClient struct {
clusterWorkflowTemplateGetter templateresolution.ClusterWorkflowTemplateGetter
namespacedWorkflowTemplateGetterMap map[string]templateresolution.WorkflowTemplateNamespacedGetter
}

var NotImplError error = fmt.Errorf("Not implemented for offline client, only valid for kind '--kinds=workflows'")
var OfflineErr = fmt.Errorf("not supported when you are in offline mode")

var _ Client = &offlineClient{}

func newOfflineClient() (context.Context, Client, error) {
return context.Background(), &offlineClient{}, nil
func newOfflineClient(files []string) (context.Context, Client, error) {
clusterWorkflowTemplateGetter := &offlineClusterWorkflowTemplateGetter{
clusterWorkflowTemplates: map[string]*wfv1.ClusterWorkflowTemplate{},
}
workflowTemplateGetters := map[string]templateresolution.WorkflowTemplateNamespacedGetter{}

for _, file := range files {
bytes, err := os.ReadFile(file)
if err != nil {
return nil, nil, fmt.Errorf("failed to read file %s: %w", file, err)
}
var generic map[string]interface{}
if err := yaml.Unmarshal(bytes, &generic); err != nil {
return nil, nil, fmt.Errorf("failed to parse YAML from file %s: %w", file, err)
}
switch generic["kind"] {
case "ClusterWorkflowTemplate":
cwftmpl := new(v1alpha1.ClusterWorkflowTemplate)
if err := yaml.Unmarshal(bytes, &cwftmpl); err != nil {
return nil, nil, fmt.Errorf("failed to unmarshal file %s as a ClusterWorkflowTemplate: %w", file, err)
}
clusterWorkflowTemplateGetter.clusterWorkflowTemplates[cwftmpl.Name] = cwftmpl

case "WorkflowTemplate":
wftmpl := new(v1alpha1.WorkflowTemplate)
if err := yaml.Unmarshal(bytes, &wftmpl); err != nil {
return nil, nil, fmt.Errorf("failed to unmarshal file %s as a WorkflowTemplate: %w", file, err)
}
getter, ok := workflowTemplateGetters[wftmpl.Namespace]
if !ok {
getter = &offlineWorkflowTemplateNamespacedGetter{
namespace: wftmpl.Namespace,
workflowTemplates: map[string]*wfv1.WorkflowTemplate{},
}
workflowTemplateGetters[wftmpl.Namespace] = getter
}

getter.(*offlineWorkflowTemplateNamespacedGetter).workflowTemplates[wftmpl.Name] = wftmpl
}

}

return context.Background(), &offlineClient{
clusterWorkflowTemplateGetter: clusterWorkflowTemplateGetter,
namespacedWorkflowTemplateGetterMap: workflowTemplateGetters,
}, nil
}

func (c *offlineClient) NewWorkflowServiceClient() workflowpkg.WorkflowServiceClient {
return &errorTranslatingWorkflowServiceClient{OfflineWorkflowServiceClient{
clusterWorkflowTemplateGetter: c.clusterWorkflowTemplateGetter,
namespacedWorkflowTemplateGetterMap: c.namespacedWorkflowTemplateGetterMap,
}}
}

func (c *offlineClient) NewCronWorkflowServiceClient() (cronworkflow.CronWorkflowServiceClient, error) {
return &errorTranslatingCronWorkflowServiceClient{OfflineCronWorkflowServiceClient{
clusterWorkflowTemplateGetter: c.clusterWorkflowTemplateGetter,
namespacedWorkflowTemplateGetterMap: c.namespacedWorkflowTemplateGetterMap,
}}, nil
}

func (c *offlineClient) NewWorkflowTemplateServiceClient() (workflowtemplate.WorkflowTemplateServiceClient, error) {
return &errorTranslatingWorkflowTemplateServiceClient{OfflineWorkflowTemplateServiceClient{
clusterWorkflowTemplateGetter: c.clusterWorkflowTemplateGetter,
namespacedWorkflowTemplateGetterMap: c.namespacedWorkflowTemplateGetterMap,
}}, nil
}

func (c *offlineClient) NewClusterWorkflowTemplateServiceClient() (clusterworkflowtemplate.ClusterWorkflowTemplateServiceClient, error) {
return &errorTranslatingWorkflowClusterTemplateServiceClient{OfflineClusterWorkflowTemplateServiceClient{
clusterWorkflowTemplateGetter: c.clusterWorkflowTemplateGetter,
namespacedWorkflowTemplateGetterMap: c.namespacedWorkflowTemplateGetterMap,
}}, nil
}

func (a *offlineClient) NewWorkflowServiceClient() workflowpkg.WorkflowServiceClient {
return &errorTranslatingWorkflowServiceClient{OfflineWorkflowServiceClient{}}
func (c *offlineClient) NewArchivedWorkflowServiceClient() (workflowarchivepkg.ArchivedWorkflowServiceClient, error) {
return nil, NoArgoServerErr
}

func (a *offlineClient) NewCronWorkflowServiceClient() (cronworkflow.CronWorkflowServiceClient, error) {
return nil, NotImplError
func (c *offlineClient) NewInfoServiceClient() (infopkg.InfoServiceClient, error) {
return nil, NoArgoServerErr
}

func (a *offlineClient) NewWorkflowTemplateServiceClient() (workflowtemplate.WorkflowTemplateServiceClient, error) {
return nil, NotImplError
type offlineWorkflowTemplateNamespacedGetter struct {
namespace string
workflowTemplates map[string]*wfv1.WorkflowTemplate
}

func (a *offlineClient) NewArchivedWorkflowServiceClient() (workflowarchivepkg.ArchivedWorkflowServiceClient, error) {
return nil, NotImplError
func (w offlineWorkflowTemplateNamespacedGetter) Get(name string) (*wfv1.WorkflowTemplate, error) {
if v, ok := w.workflowTemplates[name]; ok {
return v, nil
}
return nil, fmt.Errorf("couldn't find workflow template %q in namespace %q", name, w.namespace)
}

func (a *offlineClient) NewInfoServiceClient() (infopkg.InfoServiceClient, error) {
return nil, NotImplError
type offlineClusterWorkflowTemplateGetter struct {
clusterWorkflowTemplates map[string]*wfv1.ClusterWorkflowTemplate
}

func (a *offlineClient) NewClusterWorkflowTemplateServiceClient() (clusterworkflowtemplate.ClusterWorkflowTemplateServiceClient, error) {
return nil, NotImplError
func (o offlineClusterWorkflowTemplateGetter) Get(name string) (*wfv1.ClusterWorkflowTemplate, error) {
if v, ok := o.clusterWorkflowTemplates[name]; ok {
return v, nil
}

return nil, fmt.Errorf("couldn't find cluster workflow template %q", name)
}
47 changes: 47 additions & 0 deletions pkg/apiclient/offline-cluster-workflow-template-service-client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package apiclient

import (
"context"

"google.golang.org/grpc"

clusterworkflowtmplpkg "github.com/argoproj/argo-workflows/v3/pkg/apiclient/clusterworkflowtemplate"
"github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1"
"github.com/argoproj/argo-workflows/v3/workflow/templateresolution"
"github.com/argoproj/argo-workflows/v3/workflow/validate"
)

type OfflineClusterWorkflowTemplateServiceClient struct {
clusterWorkflowTemplateGetter templateresolution.ClusterWorkflowTemplateGetter
namespacedWorkflowTemplateGetterMap map[string]templateresolution.WorkflowTemplateNamespacedGetter
}

var _ clusterworkflowtmplpkg.ClusterWorkflowTemplateServiceClient = &OfflineClusterWorkflowTemplateServiceClient{}

func (o OfflineClusterWorkflowTemplateServiceClient) CreateClusterWorkflowTemplate(ctx context.Context, req *clusterworkflowtmplpkg.ClusterWorkflowTemplateCreateRequest, opts ...grpc.CallOption) (*v1alpha1.ClusterWorkflowTemplate, error) {
return nil, OfflineErr
}

func (o OfflineClusterWorkflowTemplateServiceClient) GetClusterWorkflowTemplate(ctx context.Context, req *clusterworkflowtmplpkg.ClusterWorkflowTemplateGetRequest, opts ...grpc.CallOption) (*v1alpha1.ClusterWorkflowTemplate, error) {
return nil, OfflineErr
}

func (o OfflineClusterWorkflowTemplateServiceClient) ListClusterWorkflowTemplates(ctx context.Context, req *clusterworkflowtmplpkg.ClusterWorkflowTemplateListRequest, opts ...grpc.CallOption) (*v1alpha1.ClusterWorkflowTemplateList, error) {
return nil, OfflineErr
}

func (o OfflineClusterWorkflowTemplateServiceClient) UpdateClusterWorkflowTemplate(ctx context.Context, req *clusterworkflowtmplpkg.ClusterWorkflowTemplateUpdateRequest, opts ...grpc.CallOption) (*v1alpha1.ClusterWorkflowTemplate, error) {
return nil, OfflineErr
}

func (o OfflineClusterWorkflowTemplateServiceClient) DeleteClusterWorkflowTemplate(ctx context.Context, req *clusterworkflowtmplpkg.ClusterWorkflowTemplateDeleteRequest, opts ...grpc.CallOption) (*clusterworkflowtmplpkg.ClusterWorkflowTemplateDeleteResponse, error) {
return nil, OfflineErr
}

func (o OfflineClusterWorkflowTemplateServiceClient) LintClusterWorkflowTemplate(ctx context.Context, req *clusterworkflowtmplpkg.ClusterWorkflowTemplateLintRequest, opts ...grpc.CallOption) (*v1alpha1.ClusterWorkflowTemplate, error) {
err := validate.ValidateClusterWorkflowTemplate(nil, o.clusterWorkflowTemplateGetter, req.Template, validate.ValidateOpts{Lint: true})
if err != nil {
return nil, err
}
return req.Template, nil
}
56 changes: 56 additions & 0 deletions pkg/apiclient/offline-cron-workflow-service-client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package apiclient

import (
"context"

"google.golang.org/grpc"

"github.com/argoproj/argo-workflows/v3/pkg/apiclient/cronworkflow"
cronworkflowpkg "github.com/argoproj/argo-workflows/v3/pkg/apiclient/cronworkflow"
"github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1"
"github.com/argoproj/argo-workflows/v3/workflow/templateresolution"
"github.com/argoproj/argo-workflows/v3/workflow/validate"
)

type OfflineCronWorkflowServiceClient struct {
clusterWorkflowTemplateGetter templateresolution.ClusterWorkflowTemplateGetter
namespacedWorkflowTemplateGetterMap map[string]templateresolution.WorkflowTemplateNamespacedGetter
}

var _ cronworkflow.CronWorkflowServiceClient = &OfflineCronWorkflowServiceClient{}

func (o OfflineCronWorkflowServiceClient) LintCronWorkflow(ctx context.Context, req *cronworkflowpkg.LintCronWorkflowRequest, _ ...grpc.CallOption) (*v1alpha1.CronWorkflow, error) {
err := validate.ValidateCronWorkflow(o.namespacedWorkflowTemplateGetterMap[req.Namespace], o.clusterWorkflowTemplateGetter, req.CronWorkflow)
if err != nil {
return nil, err
}
return req.CronWorkflow, nil
}

func (o OfflineCronWorkflowServiceClient) CreateCronWorkflow(ctx context.Context, req *cronworkflowpkg.CreateCronWorkflowRequest, _ ...grpc.CallOption) (*v1alpha1.CronWorkflow, error) {
return nil, OfflineErr
}

func (o OfflineCronWorkflowServiceClient) ListCronWorkflows(ctx context.Context, req *cronworkflowpkg.ListCronWorkflowsRequest, _ ...grpc.CallOption) (*v1alpha1.CronWorkflowList, error) {
return nil, OfflineErr
}

func (o OfflineCronWorkflowServiceClient) GetCronWorkflow(ctx context.Context, req *cronworkflowpkg.GetCronWorkflowRequest, _ ...grpc.CallOption) (*v1alpha1.CronWorkflow, error) {
return nil, OfflineErr
}

func (o OfflineCronWorkflowServiceClient) UpdateCronWorkflow(ctx context.Context, req *cronworkflowpkg.UpdateCronWorkflowRequest, _ ...grpc.CallOption) (*v1alpha1.CronWorkflow, error) {
return nil, OfflineErr
}

func (o OfflineCronWorkflowServiceClient) DeleteCronWorkflow(ctx context.Context, req *cronworkflowpkg.DeleteCronWorkflowRequest, _ ...grpc.CallOption) (*cronworkflowpkg.CronWorkflowDeletedResponse, error) {
return nil, OfflineErr
}

func (o OfflineCronWorkflowServiceClient) ResumeCronWorkflow(ctx context.Context, req *cronworkflowpkg.CronWorkflowResumeRequest, _ ...grpc.CallOption) (*v1alpha1.CronWorkflow, error) {
return nil, OfflineErr
}

func (o OfflineCronWorkflowServiceClient) SuspendCronWorkflow(ctx context.Context, req *cronworkflowpkg.CronWorkflowSuspendRequest, _ ...grpc.CallOption) (*v1alpha1.CronWorkflow, error) {
return nil, OfflineErr
}
23 changes: 6 additions & 17 deletions pkg/apiclient/offline-workflow-service-client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@ package apiclient

import (
"context"
"fmt"

"google.golang.org/grpc"

workflowpkg "github.com/argoproj/argo-workflows/v3/pkg/apiclient/workflow"
wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1"
"github.com/argoproj/argo-workflows/v3/workflow/templateresolution"
"github.com/argoproj/argo-workflows/v3/workflow/validate"
)

var OfflineErr = fmt.Errorf("not supported when you are in offline mode")

type OfflineWorkflowServiceClient struct{}
type OfflineWorkflowServiceClient struct {
clusterWorkflowTemplateGetter templateresolution.ClusterWorkflowTemplateGetter
namespacedWorkflowTemplateGetterMap map[string]templateresolution.WorkflowTemplateNamespacedGetter
}

var _ workflowpkg.WorkflowServiceClient = &OfflineWorkflowServiceClient{}

Expand Down Expand Up @@ -69,20 +70,8 @@ func (o OfflineWorkflowServiceClient) SetWorkflow(context.Context, *workflowpkg.
return nil, OfflineErr
}

type offlineWorkflowTemplateNamespacedGetter struct{}

func (w offlineWorkflowTemplateNamespacedGetter) Get(name string) (*wfv1.WorkflowTemplate, error) {
return nil, OfflineErr
}

type offlineClusterWorkflowTemplateNamespacedGetter struct{}

func (o offlineClusterWorkflowTemplateNamespacedGetter) Get(name string) (*wfv1.ClusterWorkflowTemplate, error) {
return nil, OfflineErr
}

func (o OfflineWorkflowServiceClient) LintWorkflow(_ context.Context, req *workflowpkg.WorkflowLintRequest, _ ...grpc.CallOption) (*wfv1.Workflow, error) {
err := validate.ValidateWorkflow(&offlineWorkflowTemplateNamespacedGetter{}, &offlineClusterWorkflowTemplateNamespacedGetter{}, req.Workflow, validate.ValidateOpts{Lint: true})
err := validate.ValidateWorkflow(o.namespacedWorkflowTemplateGetterMap[req.Namespace], o.clusterWorkflowTemplateGetter, req.Workflow, validate.ValidateOpts{Lint: true})
if err != nil {
return nil, err
}
Expand Down
Loading

0 comments on commit 8973fe4

Please sign in to comment.