Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TKNECO-85: Catalog Contract and Subcommands #46

Merged
merged 5 commits into from
Sep 25, 2023
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ OUTPUT_DIR ?= bin
BIN = $(OUTPUT_DIR)/$(APP)

CMD ?= ./cmd/$(APP)/...
PKG ?= ./pkg/...
PKG ?= ./internal/...

GOFLAGS ?= -v
GOFLAGS_TEST ?= -v -cover
Expand Down
315 changes: 155 additions & 160 deletions go.mod

Large diffs are not rendered by default.

1,522 changes: 409 additions & 1,113 deletions go.sum

Large diffs are not rendered by default.

99 changes: 99 additions & 0 deletions internal/attestation/attestation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package attestation

import (
"context"

"github.com/sigstore/cosign/v2/cmd/cosign/cli/generate"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/options"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/sign"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/verify"
)

// Attestation controls the sining and verification of resources.
type Attestation struct {
rootOptions *options.RootOptions // general cosign settings
keyOpts options.KeyOpts // public/private key reference

privateKeyPass []byte // stores the private key for signing
base64 bool // stores the signature using base64
outputCertificate string // output certificate location
tlogUpload bool // transaction log upload

offline bool // offline verification
ignoreSCT bool // ignore embedded SCT proof
ignoreTlog bool // ignore transaction log
}

// GetPass prompts for the user private-key password only once, when the password is already
// stored it returns instead.
func (a *Attestation) GetPass(confirm bool) ([]byte, error) {
if len(a.privateKeyPass) > 0 {
return a.privateKeyPass, nil
}

var err error
a.privateKeyPass, err = generate.GetPass(confirm)
return a.privateKeyPass, err
}

// Sign signs the resource file (first argument) on the specificed signature location.
func (a *Attestation) Sign(payloadPath, outputSignature string) error {
_, err := sign.SignBlobCmd(
a.rootOptions,
a.keyOpts,
payloadPath,
a.base64,
outputSignature,
a.outputCertificate,
a.tlogUpload,
)
return err
}

// Verify verifies the resource signature
func (a *Attestation) Verify(ctx context.Context, blobRef, sigRef string) error {
v := verify.VerifyBlobCmd{
KeyOpts: a.keyOpts,
SigRef: sigRef,
IgnoreSCT: a.ignoreSCT,
IgnoreTlog: a.ignoreTlog,
Offline: a.offline,
}
return v.Exec(ctx, blobRef)
}

// NewAttestation instantiate the Attestation helper setting the default parameters expected
// for signing and verifying resources.
func NewAttestation(key string) (*Attestation, error) {
o := &options.SignBlobOptions{}
oidcClientSecret, err := o.OIDC.ClientSecret()
if err != nil {
return nil, err
}

keyOpts := options.KeyOpts{
KeyRef: key,
Sk: false,
Slot: "",
FulcioURL: options.DefaultFulcioURL,
RekorURL: options.DefaultRekorURL,
OIDCIssuer: options.DefaultOIDCIssuerURL,
OIDCClientID: "sigstore",
OIDCClientSecret: oidcClientSecret,
SkipConfirmation: true,
}

a := &Attestation{
rootOptions: &options.RootOptions{},
keyOpts: keyOpts,
base64: true,
ignoreSCT: false,
ignoreTlog: true,
offline: true,
outputCertificate: "",
tlogUpload: false,
}
a.keyOpts.PassFunc = a.GetPass

return a, nil
}
2 changes: 1 addition & 1 deletion internal/cmd/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func (l *LintCmd) Run(cfg *config.Config) error {
errs := []error{}

err := filepath.Walk(l.resource,
func(path string, info os.FileInfo, err error) error {
func(path string, _ os.FileInfo, err error) error {
if err != nil {
return err
}
Expand Down
151 changes: 151 additions & 0 deletions internal/cmd/release.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package cmd

import (
"errors"
"fmt"
"os"
"strings"

"github.com/openshift-pipelines/tektoncd-catalog/internal/config"
"github.com/openshift-pipelines/tektoncd-catalog/internal/contract"
"github.com/openshift-pipelines/tektoncd-catalog/internal/resource"
"github.com/openshift-pipelines/tektoncd-catalog/internal/runner"

"github.com/spf13/cobra"
)

// ReleaseCmd creates a contract (".catalog.yaml") based on Tekton resources files.
type ReleaseCmd struct {
cmd *cobra.Command // cobra command definition
version string // release version
files []string // tekton resource files
output string // output file, where the contract will be written
}

var _ runner.SubCommand = &ReleaseCmd{}

const releaseLongDescription = `# catalog-cd release

Creates a contract file (".catalog.yaml") for the Tekton resources specified on
the last argument(s), the contract is stored on the "--output" location, or by
default ".catalog.yaml" on the current directory.

The following examples will store the ".catalog.yaml" on the current directory, in
order to change its location see "--output" flag.

# release all "*.yaml" files on the subdirectory
$ catalog-cd release --version="0.0.1" path/to/tekton/files/*.yaml

# release all "*.{yml|yaml}" files on the current directory
$ catalog-cd release --version="0.0.1" *.yml *.yaml

# release all "*.yml" and "*.yaml" files from the current directory
$ catalog-cd release --version="0.0.1"

It always require the "--version" flag specifying the common revision for all
resources in scope.
`

// Cmd exposes the cobra command instance.
func (r *ReleaseCmd) Cmd() *cobra.Command {
return r.cmd
}

// gatherGlogPatternFromArgs creates a set of glob patterns based on the final command line
// arguments (args), when the slice is empty it assumes the current working directory.
func (*ReleaseCmd) gatherGlogPatternFromArgs(args []string) ([]string, error) {
patterns := []string{}

if len(args) == 0 {
wd, err := os.Getwd()
if err != nil {
return nil, err
}
patterns = append(patterns, wd)
fmt.Printf("# Using current directory: %q\n", wd)
} else {
patterns = append(patterns, args...)
}

return patterns, nil
}

// Complete creates the "release" scope by finding all Tekton resource files using the cli
// args glob pattern(s).
func (r *ReleaseCmd) Complete(_ *config.Config, args []string) error {
// making sure the output flag is informed before attempt to search files
if r.output == "" {
return fmt.Errorf("--output flag is not informed")
}

// putting together a slice of glob patterns to search tekton files
patterns, err := r.gatherGlogPatternFromArgs(args)
if err != nil {
return err
}

// going through the pattern slice collected before to select the tekton resource files
// to be part of the current release, in other words, release scope
fmt.Printf("# Scan Tekton resources on: %s\n", strings.Join(patterns, ", "))
for _, pattern := range patterns {
files, err := resource.Scanner(pattern)
if err != nil {
return err
}
r.files = append(r.files, files...)
}
return nil
}

// Validate assert the release scope is not empty.
func (r *ReleaseCmd) Validate() error {
if len(r.files) == 0 {
return fmt.Errorf("no tekton resource files have been found")
}
fmt.Printf("# Found %d files to inspect!\n", len(r.files))
return nil
}

// Run creates a ".catalog.yaml" (contract file) with the release scope, saves the contract
// on the location informed by the "--output" flag.
func (r *ReleaseCmd) Run(_ *config.Config) error {
c := contract.NewContractEmpty()

fmt.Printf("# Generating contract for release %q...\n", r.version)
for _, f := range r.files {
fmt.Printf("# Loading resource file: %q\n", f)
if err := c.AddResourceFile(f, r.version); err != nil {
if errors.Is(err, contract.ErrTektonResourceUnsupported) {
return err
}
fmt.Printf("# WARNING: Skipping file %q!\n", f)
}
}

fmt.Printf("# Saving release contract at %q\n", r.output)
return c.SaveAs(r.output)
}

// NewReleaseCmd instantiates the NewReleaseCmd subcommand and flags.
func NewReleaseCmd() runner.SubCommand {
r := &ReleaseCmd{
cmd: &cobra.Command{
Use: "release [flags] [glob|directory]",
Short: "Creates a contract for Tekton resource files",
Long: releaseLongDescription,
Args: cobra.ArbitraryArgs,
SilenceUsage: true,
},
}

f := r.cmd.PersistentFlags()

f.StringVar(&r.version, "version", "", "release version")
f.StringVar(&r.output, "output", contract.Filename, "path to the contract file")

if err := r.cmd.MarkPersistentFlagRequired("version"); err != nil {
panic(err)
}

return r
}
2 changes: 2 additions & 0 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ func NewRootCmd(stream *tkncli.Stream) *cobra.Command {
rootCmd.AddCommand(runner.NewRunner(cfg, NewRenderCmd()).Cmd())
rootCmd.AddCommand(runner.NewRunner(cfg, NewVerifyCmd()).Cmd())
rootCmd.AddCommand(runner.NewRunner(cfg, NewGenerateCmd()).Cmd())
rootCmd.AddCommand(runner.NewRunner(cfg, NewReleaseCmd()).Cmd())
rootCmd.AddCommand(runner.NewRunner(cfg, NewSignCmd()).Cmd())

return rootCmd
}
85 changes: 85 additions & 0 deletions internal/cmd/sign.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package cmd

import (
"fmt"

"github.com/openshift-pipelines/tektoncd-catalog/internal/attestation"
"github.com/openshift-pipelines/tektoncd-catalog/internal/config"
"github.com/openshift-pipelines/tektoncd-catalog/internal/contract"
"github.com/openshift-pipelines/tektoncd-catalog/internal/runner"

"github.com/spf13/cobra"
)

// SignCmd subcommand "sign" to handles signing contract resources.
type SignCmd struct {
cmd *cobra.Command // cobra command definition
c *contract.Contract // catalog contract instance

privateKey string // private key location
}

var _ runner.SubCommand = &SignCmd{}

const signLongDescription = `# catalog-cd sign

Sign the catalog contract resources on the informed directory, or catalog file. By default it
assumes the current directory.

To sign the resources the subcommand requires a private-key ("--private-key" flag), and may
ask for the password when trying to interact with a encripted key.
`

// Cmd exposes the cobra command instance.
func (s *SignCmd) Cmd() *cobra.Command {
return s.cmd
}

// Complete loads the contract file from the location informed on the first argument.
func (s *SignCmd) Complete(_ *config.Config, args []string) error {
var err error
s.c, err = LoadContractFromArgs(args)
return err
}

// Validate implements runner.SubCommand.
func (*SignCmd) Validate() error {
return nil
}

// Run perform the resource signing.
func (s *SignCmd) Run(_ *config.Config) error {
helper, err := attestation.NewAttestation(s.privateKey)
if err != nil {
return err
}
if err = s.c.SignResources(func(payladPath, outputSignature string) error {
fmt.Printf("# Signing resource %q on %q...\n", payladPath, outputSignature)
return helper.Sign(payladPath, outputSignature)
}); err != nil {
return err
}
return s.c.Save()
}

// NewSignCmd instantiate the SignCmd and flags.
func NewSignCmd() runner.SubCommand {
s := &SignCmd{
cmd: &cobra.Command{
Use: "sign [flags]",
Short: "Signs Tekton Pipelines resources",
Long: signLongDescription,
Args: cobra.MaximumNArgs(1),
SilenceUsage: true,
},
}

f := s.cmd.PersistentFlags()
f.StringVar(&s.privateKey, "private-key", "", "private key file location")

if err := s.cmd.MarkPersistentFlagRequired("private-key"); err != nil {
panic(err)
}

return s
}
13 changes: 13 additions & 0 deletions internal/cmd/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package cmd

import "github.com/openshift-pipelines/tektoncd-catalog/internal/contract"

func LoadContractFromArgs(args []string) (*contract.Contract, error) {
var location string
if len(args) == 0 {
location = "."
} else {
location = args[0]
}
return contract.NewContractFromFile(location)
}
Loading