Skip to content

Commit f355374

Browse files
authored
feat(crafting-schema): add schema v2 support to cli (#2467)
Signed-off-by: Sylwester Piskozub <sylwesterpiskozub@gmail.com>
1 parent 67fc292 commit f355374

File tree

14 files changed

+588
-198
lines changed

14 files changed

+588
-198
lines changed

app/cli/cmd/workflow_contract_apply.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323

2424
func newWorkflowContractApplyCmd() *cobra.Command {
2525
var contractPath, name, description, projectName string
26+
var contractName string
2627

2728
cmd := &cobra.Command{
2829
Use: "apply",
@@ -31,13 +32,23 @@ func newWorkflowContractApplyCmd() *cobra.Command {
3132
or update it if it already exists.`,
3233
Example: ` # Apply a contract from file
3334
chainloop workflow contract apply --contract my-contract.yaml --name my-contract --project my-project`,
35+
PreRunE: func(_ *cobra.Command, _ []string) error {
36+
// Validate and extract the contract name
37+
var err error
38+
contractName, err = action.ValidateAndExtractName(name, contractPath)
39+
if err != nil {
40+
return err
41+
}
42+
43+
return nil
44+
},
3445
RunE: func(cmd *cobra.Command, _ []string) error {
3546
var desc *string
3647
if cmd.Flags().Changed("description") {
3748
desc = &description
3849
}
3950

40-
res, err := action.NewWorkflowContractApply(ActionOpts).Run(cmd.Context(), name, contractPath, desc, projectName)
51+
res, err := action.NewWorkflowContractApply(ActionOpts).Run(cmd.Context(), contractName, contractPath, desc, projectName)
4152
if err != nil {
4253
return err
4354
}
@@ -48,8 +59,6 @@ or update it if it already exists.`,
4859
}
4960

5061
cmd.Flags().StringVar(&name, "name", "", "contract name")
51-
err := cmd.MarkFlagRequired("name")
52-
cobra.CheckErr(err)
5362

5463
cmd.Flags().StringVarP(&contractPath, "contract", "f", "", "path or URL to the contract schema")
5564
cmd.Flags().StringVar(&description, "description", "", "description of the contract")

app/cli/cmd/workflow_contract_create.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,27 @@ import (
2323

2424
func newWorkflowContractCreateCmd() *cobra.Command {
2525
var name, description, contractPath, projectName string
26+
var contractName string
2627

2728
cmd := &cobra.Command{
2829
Use: "create",
2930
Short: "Create a new contract",
31+
PreRunE: func(_ *cobra.Command, _ []string) error {
32+
// Validate and extract the contract name
33+
var err error
34+
contractName, err = action.ValidateAndExtractName(name, contractPath)
35+
if err != nil {
36+
return err
37+
}
38+
39+
return nil
40+
},
3041
RunE: func(cmd *cobra.Command, args []string) error {
3142
var desc *string
3243
if cmd.Flags().Changed("description") {
3344
desc = &description
3445
}
35-
res, err := action.NewWorkflowContractCreate(ActionOpts).Run(name, desc, contractPath, projectName)
46+
res, err := action.NewWorkflowContractCreate(ActionOpts).Run(contractName, desc, contractPath, projectName)
3647
if err != nil {
3748
return err
3849
}
@@ -43,9 +54,6 @@ func newWorkflowContractCreateCmd() *cobra.Command {
4354
}
4455

4556
cmd.Flags().StringVar(&name, "name", "", "contract name")
46-
err := cmd.MarkFlagRequired("name")
47-
cobra.CheckErr(err)
48-
4957
cmd.Flags().StringVarP(&contractPath, "contract", "f", "", "path or URL to the contract schema")
5058
cmd.Flags().StringVar(&description, "description", "", "description of the contract")
5159
cmd.Flags().StringVar(&projectName, "project", "", "project name used to scope the contract, if not set the contract will be created in the organization")

app/cli/cmd/workflow_contract_update.go

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2024 The Chainloop Authors.
2+
// Copyright 2024-2025 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -16,22 +16,24 @@
1616
package cmd
1717

1818
import (
19-
"errors"
20-
2119
"github.com/chainloop-dev/chainloop/app/cli/cmd/output"
2220
"github.com/chainloop-dev/chainloop/app/cli/pkg/action"
2321
"github.com/spf13/cobra"
2422
)
2523

2624
func newWorkflowContractUpdateCmd() *cobra.Command {
2725
var name, description, contractPath string
26+
var contractName string
2827

2928
cmd := &cobra.Command{
3029
Use: "update",
3130
Short: "Update an existing contract",
32-
PreRunE: func(cmd *cobra.Command, args []string) error {
33-
if contractPath == "" && name == "" && description == "" {
34-
return errors.New("no updates provided")
31+
PreRunE: func(_ *cobra.Command, _ []string) error {
32+
// Validate and extract the contract name
33+
var err error
34+
contractName, err = action.ValidateAndExtractName(name, contractPath)
35+
if err != nil {
36+
return err
3537
}
3638

3739
return nil
@@ -42,7 +44,7 @@ func newWorkflowContractUpdateCmd() *cobra.Command {
4244
desc = &description
4345
}
4446

45-
res, err := action.NewWorkflowContractUpdate(ActionOpts).Run(name, desc, contractPath)
47+
res, err := action.NewWorkflowContractUpdate(ActionOpts).Run(contractName, desc, contractPath)
4648
if err != nil {
4749
return err
4850
}
@@ -53,12 +55,7 @@ func newWorkflowContractUpdateCmd() *cobra.Command {
5355
}
5456

5557
cmd.Flags().StringVar(&name, "name", "", "contract name")
56-
err := cmd.MarkFlagRequired("name")
57-
cobra.CheckErr(err)
58-
5958
cmd.Flags().StringVarP(&contractPath, "contract", "f", "", "path or URL to the contract schema")
60-
61-
cobra.CheckErr(err)
6259
cmd.Flags().StringVar(&description, "description", "", "description of the contract")
6360

6461
return cmd

app/cli/pkg/action/util.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,16 @@
1616
package action
1717

1818
import (
19+
"encoding/json"
1920
"errors"
21+
"fmt"
2022
"io"
2123
"net/http"
2224
"os"
2325
"path/filepath"
2426
"strings"
27+
28+
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/unmarshal"
2529
)
2630

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

4852
return os.ReadFile(filepath.Clean(fileRef))
4953
}
54+
55+
// ValidateAndExtractName validates and extracts a name from either
56+
// an explicit name parameter OR from metadata.name in the file content.
57+
// Ensures exactly one source is provided. Returns error when:
58+
// - Neither explicit name nor metadata.name is provided
59+
// - Both explicit name and metadata.name are provided (ambiguous)
60+
func ValidateAndExtractName(explicitName, filePath string) (string, error) {
61+
// Load file content if provided
62+
var content []byte
63+
var err error
64+
if filePath != "" {
65+
content, err = LoadFileOrURL(filePath)
66+
if err != nil {
67+
return "", fmt.Errorf("load file: %w", err)
68+
}
69+
}
70+
71+
// Extract name from v2 metadata (if present)
72+
metadataName, err := extractNameFromMetadata(content)
73+
if err != nil {
74+
return "", fmt.Errorf("parse content: %w", err)
75+
}
76+
77+
// Both provided - ambiguous
78+
if explicitName != "" && metadataName != "" {
79+
return "", fmt.Errorf("conflicting names: explicit name (%q) and metadata.name (%q) both provided", explicitName, metadataName)
80+
}
81+
82+
// Neither provided - missing required name
83+
if explicitName == "" && metadataName == "" {
84+
if len(content) == 0 {
85+
return "", errors.New("name is required when no file is provided")
86+
}
87+
return "", errors.New("name is required: either provide explicit name or include metadata.name in the schema")
88+
}
89+
90+
// Return whichever name was provided
91+
if explicitName != "" {
92+
return explicitName, nil
93+
}
94+
return metadataName, nil
95+
}
96+
97+
// metadataWithName represents a partial structure to extract metadata.name field
98+
type metadataWithName struct {
99+
Metadata struct {
100+
Name string `json:"name"`
101+
} `json:"metadata"`
102+
}
103+
104+
// extractNameFromMetadata attempts to extract the name from metadata.name.
105+
func extractNameFromMetadata(content []byte) (string, error) {
106+
if len(content) == 0 {
107+
return "", nil
108+
}
109+
110+
// Identify the format
111+
format, err := unmarshal.IdentifyFormat(content)
112+
if err != nil {
113+
return "", err
114+
}
115+
116+
// Convert to JSON for consistent unmarshaling
117+
var jsonData []byte
118+
switch format {
119+
case unmarshal.RawFormatJSON:
120+
jsonData = content
121+
case unmarshal.RawFormatYAML:
122+
jsonData, err = unmarshal.LoadJSONBytes(content, ".yaml")
123+
if err != nil {
124+
return "", err
125+
}
126+
case unmarshal.RawFormatCUE:
127+
jsonData, err = unmarshal.LoadJSONBytes(content, ".cue")
128+
if err != nil {
129+
return "", err
130+
}
131+
default:
132+
return "", fmt.Errorf("unsupported format: %s", format)
133+
}
134+
135+
// Unmarshal just the metadata field
136+
var schema metadataWithName
137+
if err := json.Unmarshal(jsonData, &schema); err != nil {
138+
// Not a v2 schema or invalid format
139+
return "", nil
140+
}
141+
142+
return schema.Metadata.Name, nil
143+
}

0 commit comments

Comments
 (0)