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
15 changes: 12 additions & 3 deletions app/cli/cmd/workflow_contract_apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (

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

cmd := &cobra.Command{
Use: "apply",
Expand All @@ -31,13 +32,23 @@ func newWorkflowContractApplyCmd() *cobra.Command {
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`,
PreRunE: func(_ *cobra.Command, _ []string) error {
// Validate and extract the contract name
var err error
contractName, err = action.ValidateAndExtractName(name, contractPath)
if err != nil {
return err
}

return nil
},
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)
res, err := action.NewWorkflowContractApply(ActionOpts).Run(cmd.Context(), contractName, contractPath, desc, projectName)
if err != nil {
return err
}
Expand All @@ -48,8 +59,6 @@ or update it if it already exists.`,
}

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")
Expand Down
16 changes: 12 additions & 4 deletions app/cli/cmd/workflow_contract_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,27 @@ import (

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

cmd := &cobra.Command{
Use: "create",
Short: "Create a new contract",
PreRunE: func(_ *cobra.Command, _ []string) error {
// Validate and extract the contract name
var err error
contractName, err = action.ValidateAndExtractName(name, contractPath)
if err != nil {
return err
}

return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
var desc *string
if cmd.Flags().Changed("description") {
desc = &description
}
res, err := action.NewWorkflowContractCreate(ActionOpts).Run(name, desc, contractPath, projectName)
res, err := action.NewWorkflowContractCreate(ActionOpts).Run(contractName, desc, contractPath, projectName)
if err != nil {
return err
}
Expand All @@ -43,9 +54,6 @@ func newWorkflowContractCreateCmd() *cobra.Command {
}

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing validation


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")
Expand Down
21 changes: 9 additions & 12 deletions app/cli/cmd/workflow_contract_update.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2024 The Chainloop Authors.
// Copyright 2024-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.
Expand All @@ -16,22 +16,24 @@
package cmd

import (
"errors"

"github.com/chainloop-dev/chainloop/app/cli/cmd/output"
"github.com/chainloop-dev/chainloop/app/cli/pkg/action"
"github.com/spf13/cobra"
)

func newWorkflowContractUpdateCmd() *cobra.Command {
var name, description, contractPath string
var contractName string

cmd := &cobra.Command{
Use: "update",
Short: "Update an existing contract",
PreRunE: func(cmd *cobra.Command, args []string) error {
if contractPath == "" && name == "" && description == "" {
return errors.New("no updates provided")
PreRunE: func(_ *cobra.Command, _ []string) error {
// Validate and extract the contract name
var err error
contractName, err = action.ValidateAndExtractName(name, contractPath)
if err != nil {
return err
}

return nil
Expand All @@ -42,7 +44,7 @@ func newWorkflowContractUpdateCmd() *cobra.Command {
desc = &description
}

res, err := action.NewWorkflowContractUpdate(ActionOpts).Run(name, desc, contractPath)
res, err := action.NewWorkflowContractUpdate(ActionOpts).Run(contractName, desc, contractPath)
if err != nil {
return err
}
Expand All @@ -53,12 +55,7 @@ func newWorkflowContractUpdateCmd() *cobra.Command {
}

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")

cobra.CheckErr(err)
cmd.Flags().StringVar(&description, "description", "", "description of the contract")

return cmd
Expand Down
94 changes: 94 additions & 0 deletions app/cli/pkg/action/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@
package action

import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"

"github.com/chainloop-dev/chainloop/app/controlplane/pkg/unmarshal"
)

// LoadFileOrURL loads a file from a local path or a URL
Expand All @@ -47,3 +51,93 @@ func LoadFileOrURL(fileRef string) ([]byte, error) {

return os.ReadFile(filepath.Clean(fileRef))
}

// ValidateAndExtractName validates and extracts a name from either
// an explicit name parameter OR from metadata.name in the file content.
// Ensures exactly one source is provided. Returns error when:
// - Neither explicit name nor metadata.name is provided
// - Both explicit name and metadata.name are provided (ambiguous)
func ValidateAndExtractName(explicitName, filePath string) (string, error) {
// Load file content if provided
var content []byte
var err error
if filePath != "" {
content, err = LoadFileOrURL(filePath)
if err != nil {
return "", fmt.Errorf("load file: %w", err)
}
}

// Extract name from v2 metadata (if present)
metadataName, err := extractNameFromMetadata(content)
if err != nil {
return "", fmt.Errorf("parse content: %w", err)
}

// Both provided - ambiguous
if explicitName != "" && metadataName != "" {
return "", fmt.Errorf("conflicting names: explicit name (%q) and metadata.name (%q) both provided", explicitName, metadataName)
}

// Neither provided - missing required name
if explicitName == "" && metadataName == "" {
if len(content) == 0 {
return "", errors.New("name is required when no file is provided")
}
return "", errors.New("name is required: either provide explicit name or include metadata.name in the schema")
}

// Return whichever name was provided
if explicitName != "" {
return explicitName, nil
}
return metadataName, nil
}

// metadataWithName represents a partial structure to extract metadata.name field
type metadataWithName struct {
Metadata struct {
Name string `json:"name"`
} `json:"metadata"`
}

// extractNameFromMetadata attempts to extract the name from metadata.name.
func extractNameFromMetadata(content []byte) (string, error) {
if len(content) == 0 {
return "", nil
}

// Identify the format
format, err := unmarshal.IdentifyFormat(content)
if err != nil {
return "", err
}

// Convert to JSON for consistent unmarshaling
var jsonData []byte
switch format {
case unmarshal.RawFormatJSON:
jsonData = content
case unmarshal.RawFormatYAML:
jsonData, err = unmarshal.LoadJSONBytes(content, ".yaml")
if err != nil {
return "", err
}
case unmarshal.RawFormatCUE:
jsonData, err = unmarshal.LoadJSONBytes(content, ".cue")
if err != nil {
return "", err
}
default:
return "", fmt.Errorf("unsupported format: %s", format)
}

// Unmarshal just the metadata field
var schema metadataWithName
if err := json.Unmarshal(jsonData, &schema); err != nil {
// Not a v2 schema or invalid format
return "", nil
}

return schema.Metadata.Name, nil
}
Loading
Loading