Skip to content
Open
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
6 changes: 6 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"os"
"path/filepath"

"github.com/kitops-ml/kitops/pkg/cmd/attest"
"github.com/kitops-ml/kitops/pkg/cmd/dev"
"github.com/kitops-ml/kitops/pkg/cmd/diff"
"github.com/kitops-ml/kitops/pkg/cmd/info"
Expand All @@ -37,8 +38,10 @@ import (
"github.com/kitops-ml/kitops/pkg/cmd/pull"
"github.com/kitops-ml/kitops/pkg/cmd/push"
"github.com/kitops-ml/kitops/pkg/cmd/remove"
"github.com/kitops-ml/kitops/pkg/cmd/sign"
"github.com/kitops-ml/kitops/pkg/cmd/tag"
"github.com/kitops-ml/kitops/pkg/cmd/unpack"
"github.com/kitops-ml/kitops/pkg/cmd/verify"
"github.com/kitops-ml/kitops/pkg/cmd/version"
"github.com/kitops-ml/kitops/pkg/lib/constants"
"github.com/kitops-ml/kitops/pkg/lib/filesystem/cache"
Expand Down Expand Up @@ -167,6 +170,9 @@ func addSubcommands(rootCmd *cobra.Command) {
rootCmd.AddCommand(diff.DiffCommand())
rootCmd.AddCommand(kitimport.ImportCommand())
rootCmd.AddCommand(kitcache.CacheCommand())
rootCmd.AddCommand(sign.SignCommand())
rootCmd.AddCommand(attest.AttestCommand())
rootCmd.AddCommand(verify.VerifyCommand())
}

// Execute adds all child commands to the root command and sets flags appropriately.
Expand Down
43 changes: 43 additions & 0 deletions pkg/cmd/attest/attest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright 2025 The KitOps 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.
//
// SPDX-License-Identifier: Apache-2.0

package attest

import (
"context"
"fmt"
"os"
"os/exec"
)

func RunAttest(context context.Context, options *attestOptions) error {
_, err := exec.LookPath("cosign")
if err != nil {
fmt.Println()
return fmt.Errorf("cosign not found, please install cosign")
}
cmd := exec.CommandContext(context, "cosign", options.cosignArgs...)

cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout

err = cmd.Run()
if err != nil {
return fmt.Errorf("attestation failed: %w", err)
}
return nil
}
Comment on lines +26 to +43
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The implementations for RunAttest, RunSign, and RunVerify appear to be identical.

81 changes: 81 additions & 0 deletions pkg/cmd/attest/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright 2025 The KitOps 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.
//
// SPDX-License-Identifier: Apache-2.0

package attest

import (
"context"
"fmt"

"github.com/kitops-ml/kitops/pkg/lib/completion"
"github.com/kitops-ml/kitops/pkg/lib/constants"
"github.com/kitops-ml/kitops/pkg/output"
"github.com/spf13/cobra"
)

const (
shortDesc = "Attest the supplied container image. Use the same flags as cosign."
example = `kit attest --predicate PREDICATE_FILE --key cosign.key --tlog-upload=false IMAGE_URI`
)

type attestOptions struct {
configHome string
cosignArgs []string
}

func (opts *attestOptions) complete(ctx context.Context, args []string) error {
configHome, ok := ctx.Value(constants.ConfigKey{}).(string)
if !ok {
return fmt.Errorf("default config path not set on command context")
}
opts.configHome = configHome
opts.cosignArgs = args
return nil
}

func AttestCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "attest [flags]",
Short: shortDesc,
Example: example,
RunE: runCommand(&attestOptions{}),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) >= 1 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return completion.GetLocalModelKitsCompletion(cmd.Context(), toComplete), cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveNoSpace
},
DisableFlagParsing: true,
}

return cmd
}

func runCommand(opts *attestOptions) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error {
args = append([]string{"attest"}, args...)
if err := opts.complete(cmd.Context(), args); err != nil {
return output.Fatalf("Invalid arguments: %w", err)
}

err := RunAttest(cmd.Context(), opts)
if err != nil {
return output.Fatalf("Failed to attest: %w", err)
}
output.Infof("Attestation successful")
return nil
}
}
82 changes: 82 additions & 0 deletions pkg/cmd/sign/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright 2025 The KitOps 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.
//
// SPDX-License-Identifier: Apache-2.0

package sign

import (
"context"
"fmt"

"github.com/kitops-ml/kitops/pkg/lib/completion"
"github.com/kitops-ml/kitops/pkg/lib/constants"
"github.com/kitops-ml/kitops/pkg/output"
"github.com/spf13/cobra"
)

const (
shortDesc = "Sign the supplied container image. Use the same flags as cosign."
example = `kit sign --key cosign.key --tlog-upload=false myimage:latest`
)

type signOptions struct {
configHome string
cosignArgs []string
}

func SignCommand() *cobra.Command {

cmd := &cobra.Command{
Use: "sign [flags]",
Short: shortDesc,
Example: example,
RunE: runCommand(&signOptions{}),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) >= 1 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return completion.GetLocalModelKitsCompletion(cmd.Context(), toComplete), cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveNoSpace
},
DisableFlagParsing: true,
}

return cmd
}

func (opts *signOptions) complete(ctx context.Context, args []string) error {
configHome, ok := ctx.Value(constants.ConfigKey{}).(string)
if !ok {
return fmt.Errorf("default config path not set on command context")
}
opts.configHome = configHome
opts.cosignArgs = args
return nil
}

func runCommand(opts *signOptions) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error {
args = append([]string{"sign"}, args...)
if err := opts.complete(cmd.Context(), args); err != nil {
return output.Fatalf("Invalid arguments: %w", err)
}

err := RunSign(cmd.Context(), opts)
if err != nil {
return output.Fatalf("Failed to sign: %w", err)
}
output.Infof("Modelkit signed")
return nil
}
}
44 changes: 44 additions & 0 deletions pkg/cmd/sign/sign.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright 2025 The KitOps 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.
//
// SPDX-License-Identifier: Apache-2.0

package sign

import (
"context"
"fmt"
"os"
"os/exec"
)

func RunSign(ctx context.Context, options *signOptions) error {
_, err := exec.LookPath("cosign")
if err != nil {
fmt.Println()
return fmt.Errorf("cosign not found, please install cosign")
}

cmd := exec.CommandContext(ctx, "cosign", options.cosignArgs...)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Commands will panic with "executable file not found" when cosign binary is not installed on the system. This needs pre-flight checks before making these calls. Also is there a possibility to use cosign as a library instead of an external CLI ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Looked into use of cosign as a library. Found cosign(github.com/sigstore/cosign/v2/pkg/cosign). But it doesn't have a high level api to sign or attest. Actual signing happens in internal packages. Reviewed sigstore-go, which currently doesn't have attestation creation support.
I have added a check for cosign binary before execution of command.


cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout

err = cmd.Run()
if err != nil {
return fmt.Errorf("signing failed: %w", err)
}
return nil
}
104 changes: 104 additions & 0 deletions pkg/cmd/verify/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright 2025 The KitOps 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.
//
// SPDX-License-Identifier: Apache-2.0

package verify

import (
"context"
"fmt"
"strings"

"github.com/kitops-ml/kitops/pkg/lib/completion"
"github.com/kitops-ml/kitops/pkg/lib/constants"
"github.com/kitops-ml/kitops/pkg/output"
"github.com/spf13/cobra"
)

const (
shortDesc = "Verify the ModelKit signature and attestation. Runs both verify and verify-attestation. Use --verify.* and --verify-attestation.* for flags specific to each step."
example = `kit verify --key cosign.pub --verify.insecure-ignore-tlog=true DIGEST`
)

type verifyOptions struct {
configHome string
cosignArgs []string
}

func (opts *verifyOptions) complete(ctx context.Context, args []string) error {
configHome, ok := ctx.Value(constants.ConfigKey{}).(string)
if !ok {
return fmt.Errorf("default config path not set on command context")
}
opts.configHome = configHome
opts.cosignArgs = args

return nil
}

func VerifyCommand() *cobra.Command {

cmd := &cobra.Command{
Use: "verify [flags]",
Short: shortDesc,
Example: example,
RunE: runCommand(),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) >= 1 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return completion.GetLocalModelKitsCompletion(cmd.Context(), toComplete), cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveNoSpace
},
DisableFlagParsing: true,
}

return cmd
}

func runCommand() func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error {
commands := []string{"verify", "verify-attestation"}
argsnew := [][]string{{}, {}}
for _, val := range args {
if val, ok := strings.CutPrefix(val, "--verify."); ok {
argsnew[0] = append(argsnew[0], "--"+val)
} else if val, ok := strings.CutPrefix(val, "--verify-attestation."); ok {
argsnew[1] = append(argsnew[1], "--"+val)
} else {
argsnew[0] = append(argsnew[0], val)
argsnew[1] = append(argsnew[1], val)
}
}

opts := make([]verifyOptions, 2)

for i := range 2 {
opts = append(opts, verifyOptions{})
argsnew[i] = append([]string{commands[i]}, argsnew[i]...)
if err := opts[i].complete(cmd.Context(), argsnew[i]); err != nil {
return output.Fatalf("Invalid arguments: %w", err)
}
}

for i := range len(opts) {
err := RunVerify(cmd.Context(), opts[i])
if err != nil {
return output.Fatalf("Failed to %s: %w", commands[i], err)
}
}
output.Infof("Verification successful")
return nil
}
}
Loading