diff --git a/app/cli/cmd/apply.go b/app/cli/cmd/apply.go new file mode 100644 index 000000000..38f0146ed --- /dev/null +++ b/app/cli/cmd/apply.go @@ -0,0 +1,52 @@ +// +// Copyright 2026 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "github.com/chainloop-dev/chainloop/app/cli/pkg/action" + "github.com/spf13/cobra" +) + +func newApplyCmd() *cobra.Command { + var filePath string + + cmd := &cobra.Command{ + Use: "apply", + Short: "Apply resources from YAML files", + Long: `Apply resources from a YAML file or directory. +Supports multi-document YAML files. Each document must have a 'kind' field.`, + Example: ` # Apply resources from a single file + chainloop apply -f my-contract.yaml + + # Apply resources from a directory + chainloop apply -f ./contracts/`, + RunE: func(cmd *cobra.Command, _ []string) error { + results, err := action.NewApply(ActionOpts).Run(cmd.Context(), filePath) + if err != nil { + return err + } + + logger.Info().Msgf("%d contracts applied", len(results)) + + return nil + }, + } + + cmd.Flags().StringVarP(&filePath, "file", "f", "", "path to a YAML file or directory") + cobra.CheckErr(cmd.MarkFlagRequired("file")) + + return cmd +} diff --git a/app/cli/cmd/root.go b/app/cli/cmd/root.go index 3cd16451c..033477f69 100644 --- a/app/cli/cmd/root.go +++ b/app/cli/cmd/root.go @@ -265,7 +265,7 @@ func NewRootCmd(l zerolog.Logger) *cobra.Command { rootCmd.AddCommand(newWorkflowCmd(), newAuthCmd(), NewVersionCmd(), newAttestationCmd(), newArtifactCmd(), newConfigCmd(), newIntegrationCmd(), newOrganizationCmd(), newCASBackendCmd(), - newReferrerDiscoverCmd(), newPolicyCmd(), + newReferrerDiscoverCmd(), newPolicyCmd(), newApplyCmd(), ) // Load plugins for root command and subcommands (except completion and help) diff --git a/app/cli/documentation/cli-reference.mdx b/app/cli/documentation/cli-reference.mdx index b70f73dc4..533794dfc 100755 --- a/app/cli/documentation/cli-reference.mdx +++ b/app/cli/documentation/cli-reference.mdx @@ -12,6 +12,52 @@ and vulnerability reports-directly from their CI/CD workflows, ensuring complian The CLI operates through a contract-based workflow. Security teams define workflow contracts specifying which types of evidence must be collected and how they should be validated. Developers then use the Chainloop CLI to initialize an attestation, add the required materials, and submit the attestation for validation and storage. +## chainloop apply + +Apply resources from YAML files + +Synopsis + +Apply resources from a YAML file or directory. +Supports multi-document YAML files. Each document must have a 'kind' field. + +``` +chainloop apply [flags] +``` + +Examples + +``` +Apply resources from a single file +chainloop apply -f my-contract.yaml + +Apply resources from a directory +chainloop apply -f ./contracts/ +``` + +Options + +``` +-f, --file string path to a YAML file or directory +-h, --help help for apply +``` + +Options inherited from parent commands + +``` +--artifact-cas string URL for the Artifacts Content Addressable Storage API ($CHAINLOOP_ARTIFACT_CAS_API) (default "api.cas.chainloop.dev:443") +--artifact-cas-ca string CUSTOM CA file for the Artifacts CAS API (optional) ($CHAINLOOP_ARTIFACT_CAS_API_CA) +-c, --config string Path to an existing config file (default is $HOME/.config/chainloop/config.toml) +--control-plane string URL for the Control Plane API ($CHAINLOOP_CONTROL_PLANE_API) (default "api.cp.chainloop.dev:443") +--control-plane-ca string CUSTOM CA file for the Control Plane API (optional) ($CHAINLOOP_CONTROL_PLANE_API_CA) +--debug Enable debug/verbose logging mode +-i, --insecure Skip TLS transport during connection to the control plane ($CHAINLOOP_API_INSECURE) +-n, --org string organization name +-o, --output string Output format, valid options are json and table (default "table") +-t, --token string API token. NOTE: Alternatively use the env variable CHAINLOOP_TOKEN +-y, --yes Skip confirmation +``` + ## chainloop artifact Download or upload Artifacts to the CAS diff --git a/app/cli/pkg/action/apply.go b/app/cli/pkg/action/apply.go new file mode 100644 index 000000000..c198e24a7 --- /dev/null +++ b/app/cli/pkg/action/apply.go @@ -0,0 +1,210 @@ +// +// Copyright 2026 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package action + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" + "google.golang.org/grpc" + "gopkg.in/yaml.v3" +) + +const ( + KindContract = "Contract" +) + +// ApplyResult holds the outcome of a successfully applied resource document +type ApplyResult struct { + Kind string + Name string +} + +// YAMLDoc holds a parsed YAML document with its kind and raw bytes +type YAMLDoc struct { + Kind string + Name string + RawData []byte +} + +// Apply handles applying resources from YAML files +type Apply struct { + cfg *ActionsOpts +} + +// NewApply creates a new Apply action +func NewApply(cfg *ActionsOpts) *Apply { + return &Apply{cfg: cfg} +} + +// Run applies all resources found in the given path (file or directory) +func (a *Apply) Run(ctx context.Context, path string) ([]*ApplyResult, error) { + docs, err := ParseYAMLPath(path) + if err != nil { + return nil, err + } + + // Apply contracts + var results []*ApplyResult + for _, doc := range docs { + result := &ApplyResult{Kind: doc.Kind, Name: doc.Name} + switch doc.Kind { + case KindContract: + if err := ApplyContractFromRawData(ctx, a.cfg.CPConnection, doc.RawData); err != nil { + return results, fmt.Errorf("%s/%s: %w", doc.Kind, doc.Name, err) + } + default: + return results, fmt.Errorf("unsupported kind %q", doc.Kind) + } + results = append(results, result) + } + + return results, nil +} + +// ParseYAMLPath collects all YAML files from a path (file or directory), +// reads them, and splits multi-document files into individual YAMLDoc entries. +func ParseYAMLPath(path string) ([]*YAMLDoc, error) { + files, err := CollectYAMLFiles(path) + if err != nil { + return nil, err + } + + if len(files) == 0 { + return nil, fmt.Errorf("no YAML files found in %q", path) + } + + var allDocs []*YAMLDoc + for _, f := range files { + rawData, err := os.ReadFile(f) + if err != nil { + return nil, fmt.Errorf("reading file %s: %w", f, err) + } + + docs, err := SplitYAMLDocuments(rawData) + if err != nil { + return nil, fmt.Errorf("parsing file %s: %w", f, err) + } + + allDocs = append(allDocs, docs...) + } + + return allDocs, nil +} + +// ApplyContractFromRawData applies a single contract document using the gRPC client. +func ApplyContractFromRawData(ctx context.Context, conn *grpc.ClientConn, rawData []byte) error { + client := pb.NewWorkflowContractServiceClient(conn) + + _, err := client.Apply(ctx, &pb.WorkflowContractServiceApplyRequest{ + RawSchema: rawData, + }) + if err != nil { + return fmt.Errorf("failed to apply contract: %w", err) + } + + return nil +} + +// CollectYAMLFiles returns YAML file paths from the given path. +// If path is a file, it returns that file. If a directory, it walks recursively. +func CollectYAMLFiles(path string) ([]string, error) { + info, err := os.Stat(path) + if err != nil { + return nil, fmt.Errorf("accessing path %q: %w", path, err) + } + + if !info.IsDir() { + return []string{path}, nil + } + + var files []string + err = filepath.WalkDir(path, func(p string, d os.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + ext := strings.ToLower(filepath.Ext(p)) + if ext == ".yaml" || ext == ".yml" { + files = append(files, p) + } + return nil + }) + if err != nil { + return nil, fmt.Errorf("walking directory %q: %w", path, err) + } + + return files, nil +} + +// SplitYAMLDocuments splits a potentially multi-document YAML file into individual documents, +// extracting kind and name from each. +func SplitYAMLDocuments(rawData []byte) ([]*YAMLDoc, error) { + decoder := yaml.NewDecoder(bytes.NewReader(rawData)) + + var docs []*YAMLDoc + for { + var node yaml.Node + if err := decoder.Decode(&node); err != nil { + if errors.Is(err, io.EOF) { + break + } + return nil, fmt.Errorf("decoding YAML document: %w", err) + } + + // Marshal node back to bytes for the per-resource apply + docBytes, err := yaml.Marshal(&node) + if err != nil { + return nil, fmt.Errorf("marshalling YAML node: %w", err) + } + + // Extract kind and name via partial unmarshal + var header struct { + Kind string `yaml:"kind"` + Metadata struct { + Name string `yaml:"name"` + } `yaml:"metadata"` + } + if err := yaml.Unmarshal(docBytes, &header); err != nil { + return nil, fmt.Errorf("extracting document kind: %w", err) + } + + if header.Kind == "" { + return nil, fmt.Errorf("missing 'kind' field in YAML document") + } + + if header.Metadata.Name == "" { + return nil, fmt.Errorf("missing 'metadata.name' field in YAML document of kind %q", header.Kind) + } + + docs = append(docs, &YAMLDoc{ + Kind: header.Kind, + Name: header.Metadata.Name, + RawData: docBytes, + }) + } + + return docs, nil +} diff --git a/app/controlplane/api/controlplane/v1/workflow_contract.pb.go b/app/controlplane/api/controlplane/v1/workflow_contract.pb.go index a4504a7d9..03ad32dc0 100644 --- a/app/controlplane/api/controlplane/v1/workflow_contract.pb.go +++ b/app/controlplane/api/controlplane/v1/workflow_contract.pb.go @@ -512,6 +512,95 @@ func (*WorkflowContractServiceDeleteResponse) Descriptor() ([]byte, []int) { return file_controlplane_v1_workflow_contract_proto_rawDescGZIP(), []int{9} } +type WorkflowContractServiceApplyRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Raw representation of the contract in json, yaml or cue + RawSchema []byte `protobuf:"bytes,1,opt,name=raw_schema,json=rawSchema,proto3" json:"raw_schema,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WorkflowContractServiceApplyRequest) Reset() { + *x = WorkflowContractServiceApplyRequest{} + mi := &file_controlplane_v1_workflow_contract_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WorkflowContractServiceApplyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkflowContractServiceApplyRequest) ProtoMessage() {} + +func (x *WorkflowContractServiceApplyRequest) ProtoReflect() protoreflect.Message { + mi := &file_controlplane_v1_workflow_contract_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkflowContractServiceApplyRequest.ProtoReflect.Descriptor instead. +func (*WorkflowContractServiceApplyRequest) Descriptor() ([]byte, []int) { + return file_controlplane_v1_workflow_contract_proto_rawDescGZIP(), []int{10} +} + +func (x *WorkflowContractServiceApplyRequest) GetRawSchema() []byte { + if x != nil { + return x.RawSchema + } + return nil +} + +type WorkflowContractServiceApplyResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Result *WorkflowContractItem `protobuf:"bytes,1,opt,name=result,proto3" json:"result,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WorkflowContractServiceApplyResponse) Reset() { + *x = WorkflowContractServiceApplyResponse{} + mi := &file_controlplane_v1_workflow_contract_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WorkflowContractServiceApplyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkflowContractServiceApplyResponse) ProtoMessage() {} + +func (x *WorkflowContractServiceApplyResponse) ProtoReflect() protoreflect.Message { + mi := &file_controlplane_v1_workflow_contract_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkflowContractServiceApplyResponse.ProtoReflect.Descriptor instead. +func (*WorkflowContractServiceApplyResponse) Descriptor() ([]byte, []int) { + return file_controlplane_v1_workflow_contract_proto_rawDescGZIP(), []int{11} +} + +func (x *WorkflowContractServiceApplyResponse) GetResult() *WorkflowContractItem { + if x != nil { + return x.Result + } + return nil +} + type WorkflowContractServiceUpdateResponse_Result struct { state protoimpl.MessageState `protogen:"open.v1"` Contract *WorkflowContractItem `protobuf:"bytes,1,opt,name=contract,proto3" json:"contract,omitempty"` @@ -522,7 +611,7 @@ type WorkflowContractServiceUpdateResponse_Result struct { func (x *WorkflowContractServiceUpdateResponse_Result) Reset() { *x = WorkflowContractServiceUpdateResponse_Result{} - mi := &file_controlplane_v1_workflow_contract_proto_msgTypes[10] + mi := &file_controlplane_v1_workflow_contract_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -534,7 +623,7 @@ func (x *WorkflowContractServiceUpdateResponse_Result) String() string { func (*WorkflowContractServiceUpdateResponse_Result) ProtoMessage() {} func (x *WorkflowContractServiceUpdateResponse_Result) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_v1_workflow_contract_proto_msgTypes[10] + mi := &file_controlplane_v1_workflow_contract_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -574,7 +663,7 @@ type WorkflowContractServiceDescribeResponse_Result struct { func (x *WorkflowContractServiceDescribeResponse_Result) Reset() { *x = WorkflowContractServiceDescribeResponse_Result{} - mi := &file_controlplane_v1_workflow_contract_proto_msgTypes[11] + mi := &file_controlplane_v1_workflow_contract_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -586,7 +675,7 @@ func (x *WorkflowContractServiceDescribeResponse_Result) String() string { func (*WorkflowContractServiceDescribeResponse_Result) ProtoMessage() {} func (x *WorkflowContractServiceDescribeResponse_Result) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_v1_workflow_contract_proto_msgTypes[11] + mi := &file_controlplane_v1_workflow_contract_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -656,13 +745,19 @@ const file_controlplane_v1_workflow_contract_proto_rawDesc = "" + "$WorkflowContractServiceDeleteRequest\x12\x97\x01\n" + "\x04name\x18\x01 \x01(\tB\x82\x01\xbaH\x7f\xba\x01|\n" + "\rname.dns-1123\x12:must contain only lowercase letters, numbers, and hyphens.\x1a/this.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$')R\x04name\"'\n" + - "%WorkflowContractServiceDeleteResponse2\xf6\x04\n" + + "%WorkflowContractServiceDeleteResponse\"D\n" + + "#WorkflowContractServiceApplyRequest\x12\x1d\n" + + "\n" + + "raw_schema\x18\x01 \x01(\fR\trawSchema\"e\n" + + "$WorkflowContractServiceApplyResponse\x12=\n" + + "\x06result\x18\x01 \x01(\v2%.controlplane.v1.WorkflowContractItemR\x06result2\xec\x05\n" + "\x17WorkflowContractService\x12q\n" + "\x04List\x123.controlplane.v1.WorkflowContractServiceListRequest\x1a4.controlplane.v1.WorkflowContractServiceListResponse\x12w\n" + "\x06Create\x125.controlplane.v1.WorkflowContractServiceCreateRequest\x1a6.controlplane.v1.WorkflowContractServiceCreateResponse\x12w\n" + "\x06Update\x125.controlplane.v1.WorkflowContractServiceUpdateRequest\x1a6.controlplane.v1.WorkflowContractServiceUpdateResponse\x12}\n" + "\bDescribe\x127.controlplane.v1.WorkflowContractServiceDescribeRequest\x1a8.controlplane.v1.WorkflowContractServiceDescribeResponse\x12w\n" + - "\x06Delete\x125.controlplane.v1.WorkflowContractServiceDeleteRequest\x1a6.controlplane.v1.WorkflowContractServiceDeleteResponseBLZJgithub.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1;v1b\x06proto3" + "\x06Delete\x125.controlplane.v1.WorkflowContractServiceDeleteRequest\x1a6.controlplane.v1.WorkflowContractServiceDeleteResponse\x12t\n" + + "\x05Apply\x124.controlplane.v1.WorkflowContractServiceApplyRequest\x1a5.controlplane.v1.WorkflowContractServiceApplyResponseBLZJgithub.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1;v1b\x06proto3" var ( file_controlplane_v1_workflow_contract_proto_rawDescOnce sync.Once @@ -676,7 +771,7 @@ func file_controlplane_v1_workflow_contract_proto_rawDescGZIP() []byte { return file_controlplane_v1_workflow_contract_proto_rawDescData } -var file_controlplane_v1_workflow_contract_proto_msgTypes = make([]protoimpl.MessageInfo, 12) +var file_controlplane_v1_workflow_contract_proto_msgTypes = make([]protoimpl.MessageInfo, 14) var file_controlplane_v1_workflow_contract_proto_goTypes = []any{ (*WorkflowContractServiceListRequest)(nil), // 0: controlplane.v1.WorkflowContractServiceListRequest (*WorkflowContractServiceListResponse)(nil), // 1: controlplane.v1.WorkflowContractServiceListResponse @@ -688,37 +783,42 @@ var file_controlplane_v1_workflow_contract_proto_goTypes = []any{ (*WorkflowContractServiceDescribeResponse)(nil), // 7: controlplane.v1.WorkflowContractServiceDescribeResponse (*WorkflowContractServiceDeleteRequest)(nil), // 8: controlplane.v1.WorkflowContractServiceDeleteRequest (*WorkflowContractServiceDeleteResponse)(nil), // 9: controlplane.v1.WorkflowContractServiceDeleteResponse - (*WorkflowContractServiceUpdateResponse_Result)(nil), // 10: controlplane.v1.WorkflowContractServiceUpdateResponse.Result - (*WorkflowContractServiceDescribeResponse_Result)(nil), // 11: controlplane.v1.WorkflowContractServiceDescribeResponse.Result - (*WorkflowContractItem)(nil), // 12: controlplane.v1.WorkflowContractItem - (*IdentityReference)(nil), // 13: controlplane.v1.IdentityReference - (*WorkflowContractVersionItem)(nil), // 14: controlplane.v1.WorkflowContractVersionItem + (*WorkflowContractServiceApplyRequest)(nil), // 10: controlplane.v1.WorkflowContractServiceApplyRequest + (*WorkflowContractServiceApplyResponse)(nil), // 11: controlplane.v1.WorkflowContractServiceApplyResponse + (*WorkflowContractServiceUpdateResponse_Result)(nil), // 12: controlplane.v1.WorkflowContractServiceUpdateResponse.Result + (*WorkflowContractServiceDescribeResponse_Result)(nil), // 13: controlplane.v1.WorkflowContractServiceDescribeResponse.Result + (*WorkflowContractItem)(nil), // 14: controlplane.v1.WorkflowContractItem + (*IdentityReference)(nil), // 15: controlplane.v1.IdentityReference + (*WorkflowContractVersionItem)(nil), // 16: controlplane.v1.WorkflowContractVersionItem } var file_controlplane_v1_workflow_contract_proto_depIdxs = []int32{ - 12, // 0: controlplane.v1.WorkflowContractServiceListResponse.result:type_name -> controlplane.v1.WorkflowContractItem - 13, // 1: controlplane.v1.WorkflowContractServiceCreateRequest.project_reference:type_name -> controlplane.v1.IdentityReference - 12, // 2: controlplane.v1.WorkflowContractServiceCreateResponse.result:type_name -> controlplane.v1.WorkflowContractItem - 10, // 3: controlplane.v1.WorkflowContractServiceUpdateResponse.result:type_name -> controlplane.v1.WorkflowContractServiceUpdateResponse.Result - 11, // 4: controlplane.v1.WorkflowContractServiceDescribeResponse.result:type_name -> controlplane.v1.WorkflowContractServiceDescribeResponse.Result - 12, // 5: controlplane.v1.WorkflowContractServiceUpdateResponse.Result.contract:type_name -> controlplane.v1.WorkflowContractItem - 14, // 6: controlplane.v1.WorkflowContractServiceUpdateResponse.Result.revision:type_name -> controlplane.v1.WorkflowContractVersionItem - 12, // 7: controlplane.v1.WorkflowContractServiceDescribeResponse.Result.contract:type_name -> controlplane.v1.WorkflowContractItem - 14, // 8: controlplane.v1.WorkflowContractServiceDescribeResponse.Result.revision:type_name -> controlplane.v1.WorkflowContractVersionItem - 0, // 9: controlplane.v1.WorkflowContractService.List:input_type -> controlplane.v1.WorkflowContractServiceListRequest - 2, // 10: controlplane.v1.WorkflowContractService.Create:input_type -> controlplane.v1.WorkflowContractServiceCreateRequest - 4, // 11: controlplane.v1.WorkflowContractService.Update:input_type -> controlplane.v1.WorkflowContractServiceUpdateRequest - 6, // 12: controlplane.v1.WorkflowContractService.Describe:input_type -> controlplane.v1.WorkflowContractServiceDescribeRequest - 8, // 13: controlplane.v1.WorkflowContractService.Delete:input_type -> controlplane.v1.WorkflowContractServiceDeleteRequest - 1, // 14: controlplane.v1.WorkflowContractService.List:output_type -> controlplane.v1.WorkflowContractServiceListResponse - 3, // 15: controlplane.v1.WorkflowContractService.Create:output_type -> controlplane.v1.WorkflowContractServiceCreateResponse - 5, // 16: controlplane.v1.WorkflowContractService.Update:output_type -> controlplane.v1.WorkflowContractServiceUpdateResponse - 7, // 17: controlplane.v1.WorkflowContractService.Describe:output_type -> controlplane.v1.WorkflowContractServiceDescribeResponse - 9, // 18: controlplane.v1.WorkflowContractService.Delete:output_type -> controlplane.v1.WorkflowContractServiceDeleteResponse - 14, // [14:19] is the sub-list for method output_type - 9, // [9:14] is the sub-list for method input_type - 9, // [9:9] is the sub-list for extension type_name - 9, // [9:9] is the sub-list for extension extendee - 0, // [0:9] is the sub-list for field type_name + 14, // 0: controlplane.v1.WorkflowContractServiceListResponse.result:type_name -> controlplane.v1.WorkflowContractItem + 15, // 1: controlplane.v1.WorkflowContractServiceCreateRequest.project_reference:type_name -> controlplane.v1.IdentityReference + 14, // 2: controlplane.v1.WorkflowContractServiceCreateResponse.result:type_name -> controlplane.v1.WorkflowContractItem + 12, // 3: controlplane.v1.WorkflowContractServiceUpdateResponse.result:type_name -> controlplane.v1.WorkflowContractServiceUpdateResponse.Result + 13, // 4: controlplane.v1.WorkflowContractServiceDescribeResponse.result:type_name -> controlplane.v1.WorkflowContractServiceDescribeResponse.Result + 14, // 5: controlplane.v1.WorkflowContractServiceApplyResponse.result:type_name -> controlplane.v1.WorkflowContractItem + 14, // 6: controlplane.v1.WorkflowContractServiceUpdateResponse.Result.contract:type_name -> controlplane.v1.WorkflowContractItem + 16, // 7: controlplane.v1.WorkflowContractServiceUpdateResponse.Result.revision:type_name -> controlplane.v1.WorkflowContractVersionItem + 14, // 8: controlplane.v1.WorkflowContractServiceDescribeResponse.Result.contract:type_name -> controlplane.v1.WorkflowContractItem + 16, // 9: controlplane.v1.WorkflowContractServiceDescribeResponse.Result.revision:type_name -> controlplane.v1.WorkflowContractVersionItem + 0, // 10: controlplane.v1.WorkflowContractService.List:input_type -> controlplane.v1.WorkflowContractServiceListRequest + 2, // 11: controlplane.v1.WorkflowContractService.Create:input_type -> controlplane.v1.WorkflowContractServiceCreateRequest + 4, // 12: controlplane.v1.WorkflowContractService.Update:input_type -> controlplane.v1.WorkflowContractServiceUpdateRequest + 6, // 13: controlplane.v1.WorkflowContractService.Describe:input_type -> controlplane.v1.WorkflowContractServiceDescribeRequest + 8, // 14: controlplane.v1.WorkflowContractService.Delete:input_type -> controlplane.v1.WorkflowContractServiceDeleteRequest + 10, // 15: controlplane.v1.WorkflowContractService.Apply:input_type -> controlplane.v1.WorkflowContractServiceApplyRequest + 1, // 16: controlplane.v1.WorkflowContractService.List:output_type -> controlplane.v1.WorkflowContractServiceListResponse + 3, // 17: controlplane.v1.WorkflowContractService.Create:output_type -> controlplane.v1.WorkflowContractServiceCreateResponse + 5, // 18: controlplane.v1.WorkflowContractService.Update:output_type -> controlplane.v1.WorkflowContractServiceUpdateResponse + 7, // 19: controlplane.v1.WorkflowContractService.Describe:output_type -> controlplane.v1.WorkflowContractServiceDescribeResponse + 9, // 20: controlplane.v1.WorkflowContractService.Delete:output_type -> controlplane.v1.WorkflowContractServiceDeleteResponse + 11, // 21: controlplane.v1.WorkflowContractService.Apply:output_type -> controlplane.v1.WorkflowContractServiceApplyResponse + 16, // [16:22] is the sub-list for method output_type + 10, // [10:16] is the sub-list for method input_type + 10, // [10:10] is the sub-list for extension type_name + 10, // [10:10] is the sub-list for extension extendee + 0, // [0:10] is the sub-list for field type_name } func init() { file_controlplane_v1_workflow_contract_proto_init() } @@ -736,7 +836,7 @@ func file_controlplane_v1_workflow_contract_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_controlplane_v1_workflow_contract_proto_rawDesc), len(file_controlplane_v1_workflow_contract_proto_rawDesc)), NumEnums: 0, - NumMessages: 12, + NumMessages: 14, NumExtensions: 0, NumServices: 1, }, diff --git a/app/controlplane/api/controlplane/v1/workflow_contract.proto b/app/controlplane/api/controlplane/v1/workflow_contract.proto index 42284712e..a0e101c39 100644 --- a/app/controlplane/api/controlplane/v1/workflow_contract.proto +++ b/app/controlplane/api/controlplane/v1/workflow_contract.proto @@ -29,6 +29,7 @@ service WorkflowContractService { rpc Update(WorkflowContractServiceUpdateRequest) returns (WorkflowContractServiceUpdateResponse); rpc Describe(WorkflowContractServiceDescribeRequest) returns (WorkflowContractServiceDescribeResponse); rpc Delete(WorkflowContractServiceDeleteRequest) returns (WorkflowContractServiceDeleteResponse); + rpc Apply(WorkflowContractServiceApplyRequest) returns (WorkflowContractServiceApplyResponse); } message WorkflowContractServiceListRequest {} @@ -114,3 +115,12 @@ message WorkflowContractServiceDeleteRequest { } message WorkflowContractServiceDeleteResponse {} + +message WorkflowContractServiceApplyRequest { + // Raw representation of the contract in json, yaml or cue + bytes raw_schema = 1; +} + +message WorkflowContractServiceApplyResponse { + WorkflowContractItem result = 1; +} diff --git a/app/controlplane/api/controlplane/v1/workflow_contract_grpc.pb.go b/app/controlplane/api/controlplane/v1/workflow_contract_grpc.pb.go index 83ec2a572..bb98fd92b 100644 --- a/app/controlplane/api/controlplane/v1/workflow_contract_grpc.pb.go +++ b/app/controlplane/api/controlplane/v1/workflow_contract_grpc.pb.go @@ -39,6 +39,7 @@ const ( WorkflowContractService_Update_FullMethodName = "/controlplane.v1.WorkflowContractService/Update" WorkflowContractService_Describe_FullMethodName = "/controlplane.v1.WorkflowContractService/Describe" WorkflowContractService_Delete_FullMethodName = "/controlplane.v1.WorkflowContractService/Delete" + WorkflowContractService_Apply_FullMethodName = "/controlplane.v1.WorkflowContractService/Apply" ) // WorkflowContractServiceClient is the client API for WorkflowContractService service. @@ -50,6 +51,7 @@ type WorkflowContractServiceClient interface { Update(ctx context.Context, in *WorkflowContractServiceUpdateRequest, opts ...grpc.CallOption) (*WorkflowContractServiceUpdateResponse, error) Describe(ctx context.Context, in *WorkflowContractServiceDescribeRequest, opts ...grpc.CallOption) (*WorkflowContractServiceDescribeResponse, error) Delete(ctx context.Context, in *WorkflowContractServiceDeleteRequest, opts ...grpc.CallOption) (*WorkflowContractServiceDeleteResponse, error) + Apply(ctx context.Context, in *WorkflowContractServiceApplyRequest, opts ...grpc.CallOption) (*WorkflowContractServiceApplyResponse, error) } type workflowContractServiceClient struct { @@ -105,6 +107,15 @@ func (c *workflowContractServiceClient) Delete(ctx context.Context, in *Workflow return out, nil } +func (c *workflowContractServiceClient) Apply(ctx context.Context, in *WorkflowContractServiceApplyRequest, opts ...grpc.CallOption) (*WorkflowContractServiceApplyResponse, error) { + out := new(WorkflowContractServiceApplyResponse) + err := c.cc.Invoke(ctx, WorkflowContractService_Apply_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // WorkflowContractServiceServer is the server API for WorkflowContractService service. // All implementations must embed UnimplementedWorkflowContractServiceServer // for forward compatibility @@ -114,6 +125,7 @@ type WorkflowContractServiceServer interface { Update(context.Context, *WorkflowContractServiceUpdateRequest) (*WorkflowContractServiceUpdateResponse, error) Describe(context.Context, *WorkflowContractServiceDescribeRequest) (*WorkflowContractServiceDescribeResponse, error) Delete(context.Context, *WorkflowContractServiceDeleteRequest) (*WorkflowContractServiceDeleteResponse, error) + Apply(context.Context, *WorkflowContractServiceApplyRequest) (*WorkflowContractServiceApplyResponse, error) mustEmbedUnimplementedWorkflowContractServiceServer() } @@ -136,6 +148,9 @@ func (UnimplementedWorkflowContractServiceServer) Describe(context.Context, *Wor func (UnimplementedWorkflowContractServiceServer) Delete(context.Context, *WorkflowContractServiceDeleteRequest) (*WorkflowContractServiceDeleteResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Delete not implemented") } +func (UnimplementedWorkflowContractServiceServer) Apply(context.Context, *WorkflowContractServiceApplyRequest) (*WorkflowContractServiceApplyResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Apply not implemented") +} func (UnimplementedWorkflowContractServiceServer) mustEmbedUnimplementedWorkflowContractServiceServer() { } @@ -240,6 +255,24 @@ func _WorkflowContractService_Delete_Handler(srv interface{}, ctx context.Contex return interceptor(ctx, in, info, handler) } +func _WorkflowContractService_Apply_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(WorkflowContractServiceApplyRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WorkflowContractServiceServer).Apply(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: WorkflowContractService_Apply_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WorkflowContractServiceServer).Apply(ctx, req.(*WorkflowContractServiceApplyRequest)) + } + return interceptor(ctx, in, info, handler) +} + // WorkflowContractService_ServiceDesc is the grpc.ServiceDesc for WorkflowContractService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -267,6 +300,10 @@ var WorkflowContractService_ServiceDesc = grpc.ServiceDesc{ MethodName: "Delete", Handler: _WorkflowContractService_Delete_Handler, }, + { + MethodName: "Apply", + Handler: _WorkflowContractService_Apply_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "controlplane/v1/workflow_contract.proto", diff --git a/app/controlplane/api/gen/frontend/controlplane/v1/workflow_contract.ts b/app/controlplane/api/gen/frontend/controlplane/v1/workflow_contract.ts index 60260f75b..6abf8f303 100644 --- a/app/controlplane/api/gen/frontend/controlplane/v1/workflow_contract.ts +++ b/app/controlplane/api/gen/frontend/controlplane/v1/workflow_contract.ts @@ -66,6 +66,15 @@ export interface WorkflowContractServiceDeleteRequest { export interface WorkflowContractServiceDeleteResponse { } +export interface WorkflowContractServiceApplyRequest { + /** Raw representation of the contract in json, yaml or cue */ + rawSchema: Uint8Array; +} + +export interface WorkflowContractServiceApplyResponse { + result?: WorkflowContractItem; +} + function createBaseWorkflowContractServiceListRequest(): WorkflowContractServiceListRequest { return {}; } @@ -919,6 +928,130 @@ export const WorkflowContractServiceDeleteResponse = { }, }; +function createBaseWorkflowContractServiceApplyRequest(): WorkflowContractServiceApplyRequest { + return { rawSchema: new Uint8Array(0) }; +} + +export const WorkflowContractServiceApplyRequest = { + encode(message: WorkflowContractServiceApplyRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.rawSchema.length !== 0) { + writer.uint32(10).bytes(message.rawSchema); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): WorkflowContractServiceApplyRequest { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseWorkflowContractServiceApplyRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.rawSchema = reader.bytes(); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): WorkflowContractServiceApplyRequest { + return { rawSchema: isSet(object.rawSchema) ? bytesFromBase64(object.rawSchema) : new Uint8Array(0) }; + }, + + toJSON(message: WorkflowContractServiceApplyRequest): unknown { + const obj: any = {}; + message.rawSchema !== undefined && + (obj.rawSchema = base64FromBytes(message.rawSchema !== undefined ? message.rawSchema : new Uint8Array(0))); + return obj; + }, + + create, I>>( + base?: I, + ): WorkflowContractServiceApplyRequest { + return WorkflowContractServiceApplyRequest.fromPartial(base ?? {}); + }, + + fromPartial, I>>( + object: I, + ): WorkflowContractServiceApplyRequest { + const message = createBaseWorkflowContractServiceApplyRequest(); + message.rawSchema = object.rawSchema ?? new Uint8Array(0); + return message; + }, +}; + +function createBaseWorkflowContractServiceApplyResponse(): WorkflowContractServiceApplyResponse { + return { result: undefined }; +} + +export const WorkflowContractServiceApplyResponse = { + encode(message: WorkflowContractServiceApplyResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.result !== undefined) { + WorkflowContractItem.encode(message.result, writer.uint32(10).fork()).ldelim(); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): WorkflowContractServiceApplyResponse { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseWorkflowContractServiceApplyResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.result = WorkflowContractItem.decode(reader, reader.uint32()); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): WorkflowContractServiceApplyResponse { + return { result: isSet(object.result) ? WorkflowContractItem.fromJSON(object.result) : undefined }; + }, + + toJSON(message: WorkflowContractServiceApplyResponse): unknown { + const obj: any = {}; + message.result !== undefined && + (obj.result = message.result ? WorkflowContractItem.toJSON(message.result) : undefined); + return obj; + }, + + create, I>>( + base?: I, + ): WorkflowContractServiceApplyResponse { + return WorkflowContractServiceApplyResponse.fromPartial(base ?? {}); + }, + + fromPartial, I>>( + object: I, + ): WorkflowContractServiceApplyResponse { + const message = createBaseWorkflowContractServiceApplyResponse(); + message.result = (object.result !== undefined && object.result !== null) + ? WorkflowContractItem.fromPartial(object.result) + : undefined; + return message; + }, +}; + export interface WorkflowContractService { List( request: DeepPartial, @@ -940,6 +1073,10 @@ export interface WorkflowContractService { request: DeepPartial, metadata?: grpc.Metadata, ): Promise; + Apply( + request: DeepPartial, + metadata?: grpc.Metadata, + ): Promise; } export class WorkflowContractServiceClientImpl implements WorkflowContractService { @@ -952,6 +1089,7 @@ export class WorkflowContractServiceClientImpl implements WorkflowContractServic this.Update = this.Update.bind(this); this.Describe = this.Describe.bind(this); this.Delete = this.Delete.bind(this); + this.Apply = this.Apply.bind(this); } List( @@ -1008,6 +1146,17 @@ export class WorkflowContractServiceClientImpl implements WorkflowContractServic metadata, ); } + + Apply( + request: DeepPartial, + metadata?: grpc.Metadata, + ): Promise { + return this.rpc.unary( + WorkflowContractServiceApplyDesc, + WorkflowContractServiceApplyRequest.fromPartial(request), + metadata, + ); + } } export const WorkflowContractServiceDesc = { serviceName: "controlplane.v1.WorkflowContractService" }; @@ -1127,6 +1276,29 @@ export const WorkflowContractServiceDeleteDesc: UnaryMethodDefinitionish = { } as any, }; +export const WorkflowContractServiceApplyDesc: UnaryMethodDefinitionish = { + methodName: "Apply", + service: WorkflowContractServiceDesc, + requestStream: false, + responseStream: false, + requestType: { + serializeBinary() { + return WorkflowContractServiceApplyRequest.encode(this).finish(); + }, + } as any, + responseType: { + deserializeBinary(data: Uint8Array) { + const value = WorkflowContractServiceApplyResponse.decode(data); + return { + ...value, + toObject() { + return value; + }, + }; + }, + } as any, +}; + interface UnaryMethodDefinitionishR extends grpc.UnaryMethodDefinition { requestStream: any; responseStream: any; diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowContractServiceApplyRequest.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowContractServiceApplyRequest.jsonschema.json new file mode 100644 index 000000000..7db4b3f6a --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowContractServiceApplyRequest.jsonschema.json @@ -0,0 +1,21 @@ +{ + "$id": "controlplane.v1.WorkflowContractServiceApplyRequest.jsonschema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "patternProperties": { + "^(raw_schema)$": { + "description": "Raw representation of the contract in json, yaml or cue", + "pattern": "^[A-Za-z0-9+/]*={0,2}$", + "type": "string" + } + }, + "properties": { + "rawSchema": { + "description": "Raw representation of the contract in json, yaml or cue", + "pattern": "^[A-Za-z0-9+/]*={0,2}$", + "type": "string" + } + }, + "title": "Workflow Contract Service Apply Request", + "type": "object" +} diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowContractServiceApplyRequest.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowContractServiceApplyRequest.schema.json new file mode 100644 index 000000000..377643571 --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowContractServiceApplyRequest.schema.json @@ -0,0 +1,21 @@ +{ + "$id": "controlplane.v1.WorkflowContractServiceApplyRequest.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "patternProperties": { + "^(rawSchema)$": { + "description": "Raw representation of the contract in json, yaml or cue", + "pattern": "^[A-Za-z0-9+/]*={0,2}$", + "type": "string" + } + }, + "properties": { + "raw_schema": { + "description": "Raw representation of the contract in json, yaml or cue", + "pattern": "^[A-Za-z0-9+/]*={0,2}$", + "type": "string" + } + }, + "title": "Workflow Contract Service Apply Request", + "type": "object" +} diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowContractServiceApplyResponse.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowContractServiceApplyResponse.jsonschema.json new file mode 100644 index 000000000..41e6d3771 --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowContractServiceApplyResponse.jsonschema.json @@ -0,0 +1,12 @@ +{ + "$id": "controlplane.v1.WorkflowContractServiceApplyResponse.jsonschema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "result": { + "$ref": "controlplane.v1.WorkflowContractItem.jsonschema.json" + } + }, + "title": "Workflow Contract Service Apply Response", + "type": "object" +} diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowContractServiceApplyResponse.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowContractServiceApplyResponse.schema.json new file mode 100644 index 000000000..c2d3a3b57 --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowContractServiceApplyResponse.schema.json @@ -0,0 +1,12 @@ +{ + "$id": "controlplane.v1.WorkflowContractServiceApplyResponse.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "result": { + "$ref": "controlplane.v1.WorkflowContractItem.schema.json" + } + }, + "title": "Workflow Contract Service Apply Response", + "type": "object" +} diff --git a/app/controlplane/internal/service/workflowcontract.go b/app/controlplane/internal/service/workflowcontract.go index d3bcaa112..0a7459f12 100644 --- a/app/controlplane/internal/service/workflowcontract.go +++ b/app/controlplane/internal/service/workflowcontract.go @@ -233,6 +233,77 @@ func (s *WorkflowContractService) Update(ctx context.Context, req *pb.WorkflowCo return &pb.WorkflowContractServiceUpdateResponse{Result: result}, nil } +func (s *WorkflowContractService) Apply(ctx context.Context, req *pb.WorkflowContractServiceApplyRequest) (*pb.WorkflowContractServiceApplyResponse, error) { + currentOrg, err := requireCurrentOrg(ctx) + if err != nil { + return nil, err + } + + // Validate and extract contract name and description from the raw schema + contractName, description, err := validateAndExtractMetadata(req.RawSchema, "", "") + if err != nil { + return nil, err + } + + token, err := entities.GetRawToken(ctx) + if err != nil { + return nil, err + } + + if err = s.contractUseCase.ValidateContractPolicies(req.RawSchema, token); err != nil { + return nil, handleUseCaseErr(err, s.log) + } + + // Check if the contract already exists + contract, err := s.contractUseCase.FindByNameInOrg(ctx, currentOrg.ID, contractName) + if err != nil && !biz.IsNotFound(err) { + return nil, handleUseCaseErr(err, s.log) + } + + if contract != nil { + // Contract exists we update + if err := s.checkContractAccess(ctx, contract, authz.PolicyWorkflowContractUpdate, false); err != nil { + return nil, err + } + + schemaWithVersion, err := s.contractUseCase.Update(ctx, currentOrg.ID, contractName, + &biz.WorkflowContractUpdateOpts{ + Description: description, + RawSchema: req.RawSchema, + }) + if err != nil { + return nil, handleUseCaseErr(err, s.log) + } + + return &pb.WorkflowContractServiceApplyResponse{Result: bizWorkFlowContractToPb(schemaWithVersion.Contract)}, nil + } + + // Contract does not exist we create + // Check organization settings for contract creation restriction + org, err := s.orgUseCase.FindByID(ctx, currentOrg.ID) + if err != nil { + return nil, handleUseCaseErr(err, s.log) + } + + if org.RestrictContractCreationToOrgAdmins { + if !canCreateContractsInRestrictedMode(ctx) { + return nil, errors.Forbidden("forbidden", "contract creation is restricted to organization administrators and service accounts. Please contact your administrator") + } + } + + schema, err := s.contractUseCase.Create(ctx, &biz.WorkflowContractCreateOpts{ + OrgID: currentOrg.ID, + Name: contractName, + Description: description, + RawSchema: req.RawSchema, + }) + if err != nil { + return nil, handleUseCaseErr(err, s.log) + } + + return &pb.WorkflowContractServiceApplyResponse{Result: bizWorkFlowContractToPb(schema)}, nil +} + func (s *WorkflowContractService) Delete(ctx context.Context, req *pb.WorkflowContractServiceDeleteRequest) (*pb.WorkflowContractServiceDeleteResponse, error) { currentOrg, err := requireCurrentOrg(ctx) if err != nil {