diff --git a/cmd/rekor-cli/app/pflag_groups.go b/cmd/rekor-cli/app/pflag_groups.go index ee30219b3..ccbf2be62 100644 --- a/cmd/rekor-cli/app/pflag_groups.go +++ b/cmd/rekor-cli/app/pflag_groups.go @@ -199,22 +199,3 @@ func ParseTypeFlag(typeStr string) (string, string, error) { } return "", "", errors.New("malformed type string") } - -func GetSupportedVersions(typeStr string) ([]string, error) { - typeStrings := strings.SplitN(typeStr, ":", 2) - tf, ok := types.TypeMap.Load(typeStrings[0]) - if !ok { - return nil, fmt.Errorf("unknown type %v", typeStrings[0]) - } - ti := tf.(func() types.TypeImpl)() - if ti == nil { - return nil, fmt.Errorf("type %v is not implemented", typeStrings[0]) - } - switch len(typeStrings) { - case 1: - return ti.SupportedVersions(), nil - case 2: - return []string{typeStrings[1]}, nil - } - return nil, errors.New("malformed type string") -} diff --git a/cmd/rekor-cli/app/pflags_test.go b/cmd/rekor-cli/app/pflags_test.go index 16f490bc6..726138ac1 100644 --- a/cmd/rekor-cli/app/pflags_test.go +++ b/cmd/rekor-cli/app/pflags_test.go @@ -23,7 +23,6 @@ import ( "os" "testing" - "github.com/google/go-cmp/cmp" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -843,32 +842,3 @@ func TestParseTypeFlag(t *testing.T) { } } } - -func TestGetSupportedVersions(t *testing.T) { - tests := []struct { - description string - typeStr string - expectedVersions []string - }{ - { - description: "intoto specified with version", - typeStr: "intoto:0.0.1", - expectedVersions: []string{"0.0.1"}, - }, { - description: "intoto no version specified", - typeStr: "intoto", - expectedVersions: []string{"0.0.2", "0.0.1"}, - }, - } - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - got, err := GetSupportedVersions(test.typeStr) - if err != nil { - t.Fatal(err) - } - if d := cmp.Diff(test.expectedVersions, got); d != "" { - t.Fatalf("got unexpected versions: %s", d) - } - }) - } -} diff --git a/cmd/rekor-cli/app/upload.go b/cmd/rekor-cli/app/upload.go index c75691cb9..b3b3c71c5 100644 --- a/cmd/rekor-cli/app/upload.go +++ b/cmd/rekor-cli/app/upload.go @@ -31,6 +31,7 @@ import ( "github.com/sigstore/rekor/cmd/rekor-cli/app/format" "github.com/sigstore/rekor/pkg/client" + gen_client "github.com/sigstore/rekor/pkg/generated/client" "github.com/sigstore/rekor/pkg/generated/client/entries" "github.com/sigstore/rekor/pkg/generated/models" "github.com/sigstore/rekor/pkg/log" @@ -72,12 +73,9 @@ var uploadCmd = &cobra.Command{ if err != nil { return nil, err } - params := entries.NewCreateLogEntryParams() - params.SetTimeout(viper.GetDuration("timeout")) + var entry models.ProposedEntry entryStr := viper.GetString("entry") - var resp *entries.CreateLogEntryCreated - if entryStr != "" { var entryReader io.Reader entryURL, err := url.Parse(entryStr) @@ -95,62 +93,34 @@ var uploadCmd = &cobra.Command{ return nil, fmt.Errorf("error processing entry file: %w", err) } } - entry, err := models.UnmarshalProposedEntry(entryReader, runtime.JSONConsumer()) + entry, err = models.UnmarshalProposedEntry(entryReader, runtime.JSONConsumer()) if err != nil { return nil, fmt.Errorf("error parsing entry file: %w", err) } - params.SetProposedEntry(entry) - r, err := rekorClient.Entries.CreateLogEntry(params) - if err != nil { - switch e := err.(type) { - case *entries.CreateLogEntryConflict: - return &uploadCmdOutput{ - Location: e.Location.String(), - AlreadyExists: true, - }, nil - default: - return nil, err - } - } - resp = r } else { - // Start with the default entry and work your way down - typeStr, _, err := ParseTypeFlag(viper.GetString("type")) - if err != nil { - return nil, err - } - supportedVersions, err := GetSupportedVersions(viper.GetString("type")) + typeStr, versionStr, err := ParseTypeFlag(viper.GetString("type")) if err != nil { return nil, err } + props := CreatePropsFromPflags() - for _, sv := range supportedVersions { - log.CliLogger.Infof("Trying to upload entry of type %s at version %s", typeStr, sv) - entry, err := types.NewProposedEntry(context.Background(), typeStr, sv, *props) - if err != nil { - return nil, fmt.Errorf("error: %w", err) - } - params.SetProposedEntry(entry) - r, err := rekorClient.Entries.CreateLogEntry(params) - if err == nil { - resp = r - break - } - switch e := err.(type) { - case *entries.CreateLogEntryConflict: - return &uploadCmdOutput{ - Location: e.Location.String(), - AlreadyExists: true, - }, nil - default: - log.CliLogger.Warnf("Failed to upload entry, will try with another supported version if one exists: %v", err) - } + entry, err = types.NewProposedEntry(context.Background(), typeStr, versionStr, *props) + if err != nil { + return nil, fmt.Errorf("error: %w", err) } } - - if resp == nil { - return nil, fmt.Errorf("no valid entry was created") + resp, err := tryUpload(rekorClient, entry) + if err != nil { + switch e := err.(type) { + case *entries.CreateLogEntryConflict: + return &uploadCmdOutput{ + Location: e.Location.String(), + AlreadyExists: true, + }, nil + default: + return nil, err + } } var newIndex int64 @@ -187,6 +157,29 @@ var uploadCmd = &cobra.Command{ }), } +func tryUpload(rekorClient *gen_client.Rekor, entry models.ProposedEntry) (*entries.CreateLogEntryCreated, error) { + params := entries.NewCreateLogEntryParams() + params.SetTimeout(viper.GetDuration("timeout")) + if pei, ok := entry.(types.ProposedEntryIterator); ok { + params.SetProposedEntry(pei.Get()) + } else { + params.SetProposedEntry(entry) + } + resp, err := rekorClient.Entries.CreateLogEntry(params) + if err != nil { + if e, ok := err.(*entries.CreateLogEntryBadRequest); ok { + if pei, ok := entry.(types.ProposedEntryIterator); ok { + if pei.HasNext() { + log.CliLogger.Errorf("failed to upload entry: %v", e) + return tryUpload(rekorClient, pei.GetNext()) + } + } + } + return nil, err + } + return resp, nil +} + func init() { initializePFlagMap() if err := addArtifactPFlags(uploadCmd); err != nil { diff --git a/pkg/types/entries.go b/pkg/types/entries.go index 55559752d..4b0ee5b98 100644 --- a/pkg/types/entries.go +++ b/pkg/types/entries.go @@ -45,6 +45,14 @@ type EntryWithAttestationImpl interface { AttestationKeyValue() (string, []byte) // returns the key to be used when storing the attestation as well as the attestation itself } +// ProposedEntryIterator is an iterator over a list of proposed entries +type ProposedEntryIterator interface { + models.ProposedEntry + HasNext() bool + Get() models.ProposedEntry + GetNext() models.ProposedEntry +} + // EntryFactory describes a factory function that can generate structs for a specific versioned type type EntryFactory func() EntryImpl diff --git a/pkg/types/intoto/intoto.go b/pkg/types/intoto/intoto.go index 0c79019c5..3905ecec4 100644 --- a/pkg/types/intoto/intoto.go +++ b/pkg/types/intoto/intoto.go @@ -21,6 +21,7 @@ import ( "fmt" "github.com/sigstore/rekor/pkg/generated/models" + "github.com/sigstore/rekor/pkg/log" "github.com/sigstore/rekor/pkg/types" "golang.org/x/exp/slices" ) @@ -60,9 +61,41 @@ func (it BaseIntotoType) UnmarshalEntry(pe models.ProposedEntry) (types.EntryImp } func (it *BaseIntotoType) CreateProposedEntry(ctx context.Context, version string, props types.ArtifactProperties) (models.ProposedEntry, error) { + var head ProposedIntotoEntryIterator + var next *ProposedIntotoEntryIterator if version == "" { + // get default version as head of list version = it.DefaultVersion() + ei, err := it.VersionedUnmarshal(nil, version) + if err != nil { + return nil, fmt.Errorf("fetching default Intoto version implementation: %w", err) + } + pe, err := ei.CreateFromArtifactProperties(ctx, props) + if err != nil { + return nil, fmt.Errorf("creating default Intoto entry: %w", err) + } + head.ProposedEntry = pe + next = &head + for _, v := range it.SupportedVersions() { + if v == it.DefaultVersion() { + continue + } + ei, err := it.VersionedUnmarshal(nil, v) + if err != nil { + log.Logger.Errorf("fetching Intoto version (%v) implementation: %w", v, err) + continue + } + versionedPE, err := ei.CreateFromArtifactProperties(ctx, props) + if err != nil { + log.Logger.Errorf("error creating Intoto entry of version (%v): %w", v, err) + continue + } + next.next = &ProposedIntotoEntryIterator{versionedPE, nil} + next = next.next.(*ProposedIntotoEntryIterator) + } + return head, nil } + ei, err := it.VersionedUnmarshal(nil, version) if err != nil { return nil, fmt.Errorf("fetching Intoto version implementation: %w", err) @@ -74,9 +107,7 @@ func (it BaseIntotoType) DefaultVersion() string { return "0.0.2" } -// SupportedVersions returns the supported versions for this type; -// it deliberately omits 0.0.1 from the list of supported versions as that -// version did not persist signatures inside the log entry +// SupportedVersions returns the supported versions for this type in the order of preference func (it BaseIntotoType) SupportedVersions() []string { return []string{"0.0.2", "0.0.1"} } @@ -85,3 +116,20 @@ func (it BaseIntotoType) SupportedVersions() []string { func (it *BaseIntotoType) IsSupportedVersion(proposedVersion string) bool { return slices.Contains(it.SupportedVersions(), proposedVersion) } + +type ProposedIntotoEntryIterator struct { + models.ProposedEntry + next models.ProposedEntry +} + +func (p ProposedIntotoEntryIterator) HasNext() bool { + return p.next != nil +} + +func (p ProposedIntotoEntryIterator) GetNext() models.ProposedEntry { + return p.next +} + +func (p ProposedIntotoEntryIterator) Get() models.ProposedEntry { + return p.ProposedEntry +} diff --git a/tests/e2e_test.go b/tests/e2e_test.go index fda0f5ce1..9408855b0 100644 --- a/tests/e2e_test.go +++ b/tests/e2e_test.go @@ -33,19 +33,16 @@ import ( "fmt" "io/ioutil" "net/http" - "net/url" "os" "os/exec" "path/filepath" "reflect" - "runtime" "strconv" "strings" "testing" "time" "golang.org/x/sync/errgroup" - "sigs.k8s.io/release-utils/version" "github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer" "github.com/go-openapi/strfmt" @@ -696,6 +693,7 @@ func TestIntotoMultiSig(t *testing.T) { } +/* func TestIntotoBlockV001(t *testing.T) { td := t.TempDir() attestationPath := filepath.Join(td, "attestation.json") @@ -789,6 +787,7 @@ func TestIntotoBlockV001(t *testing.T) { t.Fatalf("failed inserting v0.0.1 entry: %v", err) } } +*/ func TestTimestampArtifact(t *testing.T) { var out string