Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/cli/cmd/workflow_contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func newWorkflowContractCmd() *cobra.Command {
cmd.AddCommand(
newWorkflowContractListCmd(),
newWorkflowContractCreateCmd(),
newWorkflowContractApplyCmd(),
newWorkflowContractDescribeCmd(),
newWorkflowContractUpdateCmd(),
newWorkflowContractDeleteCmd(),
Expand Down
59 changes: 59 additions & 0 deletions app/cli/cmd/workflow_contract_apply.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//
// Copyright 2025 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/cmd/output"
"github.com/chainloop-dev/chainloop/app/cli/pkg/action"
"github.com/spf13/cobra"
)

func newWorkflowContractApplyCmd() *cobra.Command {
var contractPath, name, description, projectName string

cmd := &cobra.Command{
Use: "apply",
Short: "Apply a contract (create or update)",
Long: `Apply a contract from a file. This command will create the contract if it doesn't exist,
or update it if it already exists.`,
Example: ` # Apply a contract from file
chainloop workflow contract apply --contract my-contract.yaml --name my-contract --project my-project`,
RunE: func(cmd *cobra.Command, _ []string) error {
var desc *string
if cmd.Flags().Changed("description") {
desc = &description
}

res, err := action.NewWorkflowContractApply(ActionOpts).Run(cmd.Context(), name, contractPath, desc, projectName)
if err != nil {
return err
}

logger.Info().Msg("Contract applied!")
return output.EncodeOutput(flagOutputFormat, res, contractItemTableOutput)
},
}

cmd.Flags().StringVar(&name, "name", "", "contract name")
err := cmd.MarkFlagRequired("name")
cobra.CheckErr(err)

cmd.Flags().StringVarP(&contractPath, "contract", "f", "", "path or URL to the contract schema")
cmd.Flags().StringVar(&description, "description", "", "description of the contract")
cmd.Flags().StringVar(&projectName, "project", "", "project name used to scope the contract, if not set the contract will be created in the organization")

return cmd
}
46 changes: 46 additions & 0 deletions app/cli/documentation/cli-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3291,6 +3291,52 @@ Options inherited from parent commands
-y, --yes Skip confirmation
```

#### chainloop workflow contract apply

Apply a contract (create or update)

Synopsis

Apply a contract from a file. This command will create the contract if it doesn't exist,
or update it if it already exists.

```
chainloop workflow contract apply [flags]
```

Examples

```
Apply a contract from file
chainloop workflow contract apply --contract my-contract.yaml --name my-contract --project my-project
```

Options

```
-f, --contract string path or URL to the contract schema
--description string description of the contract
-h, --help help for apply
--name string contract name
--project string project name used to scope the contract, if not set the contract will be created in the organization
```

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 workflow contract create

Create a new contract
Expand Down
87 changes: 87 additions & 0 deletions app/cli/pkg/action/workflow_contract_apply.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
//
// Copyright 2025 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 (
"context"
"fmt"

pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
)

type WorkflowContractApply struct {
cfg *ActionsOpts
}

func NewWorkflowContractApply(cfg *ActionsOpts) *WorkflowContractApply {
return &WorkflowContractApply{cfg}
}

func (action *WorkflowContractApply) Run(ctx context.Context, contractName string, contractPath string, description *string, projectName string) (*WorkflowContractItem, error) {
client := pb.NewWorkflowContractServiceClient(action.cfg.CPConnection)

// Try to describe the specific contract first to determine if we should create or update
describeReq := &pb.WorkflowContractServiceDescribeRequest{
Name: contractName,
}

var rawContract []byte
if contractPath != "" {
raw, err := LoadFileOrURL(contractPath)
if err != nil {
action.cfg.Logger.Debug().Err(err).Msg("loading the contract")
return nil, err
}
rawContract = raw
}

_, err := client.Describe(ctx, describeReq)
if err == nil {
// Contract exists, perform update
updateReq := &pb.WorkflowContractServiceUpdateRequest{
Name: contractName,
Description: description,
RawContract: rawContract,
}

res, err := client.Update(ctx, updateReq)
if err != nil {
return nil, fmt.Errorf("failed to update existing contract '%s': %w", contractName, err)
}

return pbWorkflowContractItemToAction(res.Result.Contract), nil
}

// Contract doesn't exist, perform create
createReq := &pb.WorkflowContractServiceCreateRequest{
Name: contractName,
Description: description,
RawContract: rawContract,
}

if projectName != "" {
createReq.ProjectReference = &pb.IdentityReference{
Name: &projectName,
}
}

res, err := client.Create(ctx, createReq)
if err != nil {
return nil, fmt.Errorf("failed to create new contract '%s': %w", contractName, err)
}

return pbWorkflowContractItemToAction(res.Result), nil
}
Loading