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
346 changes: 183 additions & 163 deletions app/cli/api/attestation/v1/crafting_state.pb.go

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions app/cli/api/attestation/v1/crafting_state.pb.validate.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions app/cli/api/attestation/v1/crafting_state.proto
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ message Attestation {
WorkflowMetadata workflow = 3 [(validate.rules).message.required = true];

map<string, Material> materials = 4;
// Annotations for the attestation
map<string, string> annotations = 5 [(validate.rules).map.values.string.min_len = 1];

message Material {
oneof m {
Expand Down
16 changes: 16 additions & 0 deletions app/cli/cmd/attestation.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package cmd
import (
"fmt"
"os"
"strings"

"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -49,3 +50,18 @@ func newAttestationCmd() *cobra.Command {

return cmd
}

// extractAnnotations extracts the annotations from the flag and returns a map
// the expected input format is key=value
func extractAnnotations(annotationsFlag []string) (map[string]string, error) {
var annotations = make(map[string]string)
for _, annotation := range annotationsFlag {
kv := strings.Split(annotation, "=")
if len(kv) != 2 {
return nil, fmt.Errorf("invalid annotation %q, the format must be key=value", annotation)
}
annotations[kv[0]] = kv[1]
}

return annotations, nil
}
15 changes: 4 additions & 11 deletions app/cli/cmd/attestation_add.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ package cmd

import (
"errors"
"fmt"
"strings"

"github.com/spf13/cobra"
"github.com/spf13/viper"
Expand Down Expand Up @@ -48,17 +46,12 @@ func newAttestationAddCmd() *cobra.Command {
)

// Extract annotations
var annotations = make(map[string]string)
for _, annotation := range annotationsFlag {
kv := strings.SplitN(annotation, "=", 2)
if len(kv) != 2 {
return fmt.Errorf("invalid annotation %q, the format must be key=value", annotation)
}
annotations[kv[0]] = kv[1]
annotations, err := extractAnnotations(annotationsFlag)
if err != nil {
return err
}

err := a.Run(name, value, annotations)
if err != nil {
if err := a.Run(name, value, annotations); err != nil {
if errors.Is(err, action.ErrAttestationNotInitialized) {
return err
}
Expand Down
18 changes: 15 additions & 3 deletions app/cli/cmd/attestation_push.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ import (

func newAttestationPushCmd() *cobra.Command {
var pkPath string
var annotationsFlag []string
cmd := &cobra.Command{
Use: "push",
Short: "generate and push the attestation to the control plane",
Example: ` chainloop attestation push --key <key path>|<env://VAR_NAME> --token [robot-account-token]
Example: ` chainloop attestation push --key <key path>|<env://VAR_NAME> --token [robot-account-token] --annotation key=value,key2=val2

# sign the resulting attestation using a cosign key present in the filesystem and stdin for the passphrase
# NOTE that the --token flag can be replaced by having the CHAINLOOP_ROBOT_ACCOUNT env variable
Expand All @@ -39,7 +40,12 @@ func newAttestationPushCmd() *cobra.Command {

# The passphrase can be retrieved from a well-known environment variable
export CHAINLOOP_SIGNING_PASSWORD="my cosign key passphrase"
chainloop attestation push --key cosign.key`,
chainloop attestation push --key cosign.key

# You can provide values for the annotations that have previously defined in the contract for example
chainloop attestation push --annotation key=value --annotation key2=value2
# Or alternatively
chainloop attestation push --annotation key=value,key2=value2`,
Annotations: map[string]string{
useWorkflowRobotAccount: "true",
},
Expand All @@ -59,7 +65,12 @@ func newAttestationPushCmd() *cobra.Command {
ActionsOpts: actionOpts, KeyPath: pkPath, CLIVersion: info.Version, CLIDigest: info.Digest,
})

res, err := a.Run()
annotations, err := extractAnnotations(annotationsFlag)
if err != nil {
return err
}

res, err := a.Run(annotations)
if err != nil {
if errors.Is(err, action.ErrAttestationNotInitialized) {
return err
Expand All @@ -73,6 +84,7 @@ func newAttestationPushCmd() *cobra.Command {
}

cmd.Flags().StringVarP(&pkPath, "key", "k", "", "reference (path or env variable name) to the cosign private key that will be used to sign the attestation")
cmd.Flags().StringSliceVar(&annotationsFlag, "annotation", nil, "additional annotation in the format of key=value")

return cmd
}
19 changes: 18 additions & 1 deletion app/cli/cmd/attestation_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,18 @@ func attestationStatusTableOutput(status *action.AttestationStatusResult) error
gt.AppendRow(table.Row{"Runner Type", status.RunnerContext.RunnerType})
gt.AppendRow(table.Row{"Runner URL", status.RunnerContext.JobURL})
}

if len(status.Annotations) > 0 {
gt.AppendRow(table.Row{"Annotations", "------"})
for _, a := range status.Annotations {
value := a.Value
if value == "" {
value = "[NOT SET]"
}
gt.AppendRow(table.Row{"", fmt.Sprintf("%s: %s", a.Name, value)})
}
}

gt.Render()

if err := materialsTable(status); err != nil {
Expand Down Expand Up @@ -149,7 +161,12 @@ func materialsTable(status *action.AttestationStatusResult) error {
if len(m.Annotations) > 0 {
mt.AppendRow(table.Row{"Annotations", "------"})
for _, a := range m.Annotations {
mt.AppendRow(table.Row{"", fmt.Sprintf("%s: %s", a.Name, a.Value)})
value := a.Value
if value == "" {
value = "[NOT SET]"
}

mt.AppendRow(table.Row{"", fmt.Sprintf("%s: %s", a.Name, value)})
}
}

Expand Down
79 changes: 79 additions & 0 deletions app/cli/cmd/attestation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//
// Copyright 2023 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 (
"testing"

"github.com/stretchr/testify/assert"
)

func TestExtractAnnotations(t *testing.T) {
testCases := []struct {
input []string
want map[string]string
wantErr bool
}{
{
input: []string{
"foo=bar",
"baz=qux",
},
want: map[string]string{
"foo": "bar",
"baz": "qux",
},
wantErr: false,
},
{
input: []string{
"foo=bar",
"baz",
},
wantErr: true,
},
{
input: []string{
"foo=bar",
"baz=qux",
"foo=bar",
},
want: map[string]string{
"foo": "bar",
"baz": "qux",
},
wantErr: false,
},
{
input: []string{
"foo=bar",
"baz=qux=qux",
},
wantErr: true,
},
}

for _, tc := range testCases {
got, err := extractAnnotations(tc.input)
if tc.wantErr {
assert.Error(t, err)
continue
}

assert.NoError(t, err)
assert.Equal(t, tc.want, got)
}
}
41 changes: 40 additions & 1 deletion app/cli/internal/action/attestation_push.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package action
import (
"context"
"encoding/json"
"fmt"

pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
"github.com/chainloop-dev/chainloop/internal/attestation/crafter"
Expand Down Expand Up @@ -48,7 +49,7 @@ func NewAttestationPush(cfg *AttestationPushOpts) *AttestationPush {
}

// TODO: Return defined type
func (action *AttestationPush) Run() (interface{}, error) {
func (action *AttestationPush) Run(runtimeAnnotations map[string]string) (interface{}, error) {
if initialized := action.c.AlreadyInitialized(); !initialized {
return nil, ErrAttestationNotInitialized
}
Expand All @@ -58,6 +59,44 @@ func (action *AttestationPush) Run() (interface{}, error) {
return nil, err
}

// Annotations
craftedAnnotations := make(map[string]string, 0)
// 1 - Set annotations that come from the contract
for _, v := range action.c.CraftingState.InputSchema.GetAnnotations() {
craftedAnnotations[v.Name] = v.Value
}

// 2 - Populate annotation values from the ones provided at runtime
// a) we do not allow overriding values that come from the contract
// b) we do not allow adding annotations that are not defined in the contract
for kr, vr := range runtimeAnnotations {
// If the annotation is not defined in the material we fail
if v, found := craftedAnnotations[kr]; !found {
return nil, fmt.Errorf("annotation %q not found", kr)
} else if v == "" {
// Set it only if it's not set
craftedAnnotations[kr] = vr
} else {
// NOTE: we do not allow overriding values that come from the contract
action.Logger.Info().Str("annotation", kr).Msg("annotation can't be changed, skipping")
}
}

// Make sure all the annotation values are now set
// This is in fact validated below but by manually checking we can provide a better error message
for k, v := range craftedAnnotations {
var missingAnnotations []string
if v == "" {
missingAnnotations = append(missingAnnotations, k)
}

if len(missingAnnotations) > 0 {
return nil, fmt.Errorf("annotations %q required", missingAnnotations)
}
}
// Set the annotations
action.c.CraftingState.Attestation.Annotations = craftedAnnotations

if err := action.c.ValidateAttestation(); err != nil {
return nil, err
}
Expand Down
2 changes: 2 additions & 0 deletions app/cli/internal/action/attestation_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type AttestationStatusResult struct {
EnvVars map[string]string
RunnerContext *AttestationResultRunnerContext
DryRun bool
Annotations []*Annotation
}

type AttestationResultRunnerContext struct {
Expand Down Expand Up @@ -89,6 +90,7 @@ func (action *AttestationStatus) Run() (*AttestationStatusResult, error) {
},
InitializedAt: toTimePtr(att.InitializedAt.AsTime()),
DryRun: c.CraftingState.DryRun,
Annotations: pbAnnotationsToAction(c.CraftingState.InputSchema.GetAnnotations()),
}

// Materials
Expand Down
Loading