Skip to content

Commit

Permalink
add Freight and Warehouse resource types (akuity#911)
Browse files Browse the repository at this point in the history
Signed-off-by: Kent <kent.rancourt@gmail.com>
  • Loading branch information
krancour authored Oct 17, 2023
1 parent eeb0ea6 commit 535d014
Show file tree
Hide file tree
Showing 98 changed files with 8,824 additions and 4,201 deletions.
4 changes: 3 additions & 1 deletion Tiltfile
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,11 @@ k8s_resource(
k8s_resource(
new_name = 'crds',
objects = [
'freights.kargo.akuity.io:customresourcedefinition',
'stages.kargo.akuity.io:customresourcedefinition',
'promotionpolicies.kargo.akuity.io:customresourcedefinition',
'promotions.kargo.akuity.io:customresourcedefinition'
'promotions.kargo.akuity.io:customresourcedefinition',
'warehouses.kargo.akuity.io:customresourcedefinition'
],
labels = ['kargo']
)
Expand Down
69 changes: 69 additions & 0 deletions api/v1alpha1/freight_helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package v1alpha1

import (
"context"

"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)

// GetFreight returns a pointer to the Freight resource specified by the
// namespacedName argument. If no such resource is found, nil is returned
// instead.
func GetFreight(
ctx context.Context,
c client.Client,
namespacedName types.NamespacedName,
) (*Freight, error) {
freight := Freight{}
if err := c.Get(ctx, namespacedName, &freight); err != nil {
if err = client.IgnoreNotFound(err); err == nil {
return nil, nil
}
return nil, errors.Wrapf(
err,
"error getting Freight %q in namespace %q",
namespacedName.Name,
namespacedName.Namespace,
)
}
return &freight, nil
}

// GetQualifiedFreight returns a pointer to the Freight resource specified by
// the namespacedName argument if it is found and EITHER no Stages were
// specified in the function call OR the Freight has qualified for ANY of the
// specified Stages. If all other cases, nil is returned instead.
//
// Note: The rationale for returning the found Freight (if any) instead of nil
// when no Stages are specified is that the Stages provided are typically the
// names of Stages UPSTREAM from some other Stage. i.e. The typical use for this
// function is to answer whether a piece of Freight has qualified for any of a
// given Stage's UPSTREAM Stages. Some Stages have no upstream Stages, so any
// Freight that is found is implicitly qualified.
func GetQualifiedFreight(
ctx context.Context,
c client.Client,
namespacedName types.NamespacedName,
stages []string,
) (*Freight, error) {
freight, err := GetFreight(ctx, c, namespacedName)
if err != nil {
return nil, err
}
if freight == nil {
return nil, nil
}
if len(stages) == 0 {
return freight, nil
}
for qualifiedStage := range freight.Status.Qualifications {
for _, stage := range stages {
if qualifiedStage == stage {
return freight, nil
}
}
}
return nil, nil
}
64 changes: 64 additions & 0 deletions api/v1alpha1/freight_helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package v1alpha1

import (
"context"
"testing"

"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8sruntime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

func TestGetFreight(t *testing.T) {
scheme := k8sruntime.NewScheme()
require.NoError(t, SchemeBuilder.AddToScheme(scheme))

testCases := []struct {
name string
client client.Client
assertions func(*Freight, error)
}{
{
name: "not found",
client: fake.NewClientBuilder().WithScheme(scheme).Build(),
assertions: func(freight *Freight, err error) {
require.NoError(t, err)
require.Nil(t, freight)
},
},

{
name: "found",
client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(
&Freight{
ObjectMeta: metav1.ObjectMeta{
Name: "fake-freight",
Namespace: "fake-namespace",
},
},
).Build(),
assertions: func(freight *Freight, err error) {
require.NoError(t, err)
require.Equal(t, "fake-freight", freight.Name)
require.Equal(t, "fake-namespace", freight.Namespace)
},
},
}

for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
freight, err := GetFreight(
context.Background(),
testCase.client,
types.NamespacedName{
Namespace: "fake-namespace",
Name: "fake-freight",
},
)
testCase.assertions(freight, err)
})
}
}
107 changes: 107 additions & 0 deletions api/v1alpha1/freight_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package v1alpha1

import (
"crypto/sha1"
"fmt"
"sort"
"strings"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status

// Freight represents a collection of versioned artifacts.
type Freight struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
// ID is a system-assigned value that is derived deterministically from the
// contents of the Freight. i.e. Two pieces of Freight can be compared for
// equality by comparing their IDs.
ID string `json:"id,omitempty"`
// Commits describes specific Git repository commits.
Commits []GitCommit `json:"commits,omitempty"`
// Images describes specific versions of specific container images.
Images []Image `json:"images,omitempty"`
// Charts describes specific versions of specific Helm charts.
Charts []Chart `json:"charts,omitempty"`
// Status describes the current status of this Freight.
Status FreightStatus `json:"status,omitempty"`
}

func (f *Freight) GetStatus() *FreightStatus {
return &f.Status
}

// UpdateID deterministically calculates a piece of Freight's ID based on its
// contents and assigns it to the ID field.
func (f *Freight) UpdateID() {
size := len(f.Commits) + len(f.Images) + len(f.Charts)
artifacts := make([]string, 0, size)
for _, commit := range f.Commits {
artifacts = append(
artifacts,
fmt.Sprintf("%s:%s", commit.RepoURL, commit.ID),
)
}
for _, image := range f.Images {
artifacts = append(
artifacts,
fmt.Sprintf("%s:%s", image.RepoURL, image.Tag),
)
}
for _, chart := range f.Charts {
artifacts = append(
artifacts,
fmt.Sprintf("%s/%s:%s", chart.RegistryURL, chart.Name, chart.Version),
)
}
sort.Strings(artifacts)
f.ID = fmt.Sprintf(
"%x",
sha1.Sum([]byte(strings.Join(artifacts, "|"))),
)
}

// GitCommit describes a specific commit from a specific Git repository.
type GitCommit struct {
// RepoURL is the URL of a Git repository.
RepoURL string `json:"repoURL,omitempty"`
// ID is the ID of a specific commit in the Git repository specified by
// RepoURL.
ID string `json:"id,omitempty"`
// Branch denotes the branch of the repository where this commit was found.
Branch string `json:"branch,omitempty"`
// HealthCheckCommit is the ID of a specific commit. When specified,
// assessments of Stage health will used this value (instead of ID) when
// determining if applicable sources of Argo CD Application resources
// associated with the Stage are or are not synced to this commit. Note that
// there are cases (as in that of Bookkeeper being utilized as a promotion
// mechanism) wherein the value of this field may differ from the commit ID
// found in the ID field.
HealthCheckCommit string `json:"healthCheckCommit,omitempty"`
// Message is the git commit message
Message string `json:"message,omitempty"`
// Author is the git commit author
Author string `json:"author,omitempty"`
}

// FreightStatus describes a piece of Freight's most recently observed state.
type FreightStatus struct {
// Qualifications describes the Stages for which this Freight has been
// qualified.
Qualifications map[string]Qualification `json:"qualifications,omitempty"`
}

// Qualification describes a Freight's qualification for a Stage.
type Qualification struct{}

//+kubebuilder:object:root=true

// FreightList is a list of Freight resources.
type FreightList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Freight `json:"items"`
}
111 changes: 111 additions & 0 deletions api/v1alpha1/freight_types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package v1alpha1

import (
"testing"

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

func TestGitCommitEquals(t *testing.T) {
testCases := []struct {
name string
lhs *GitCommit
rhs *GitCommit
expectedResult bool
}{
{
name: "lhs and rhs both nil",
expectedResult: true,
},
{
name: "only lhs is nil",
rhs: &GitCommit{},
expectedResult: false,
},
{
name: "only rhs is nil",
lhs: &GitCommit{},
expectedResult: false,
},
{
name: "repoUrls differ",
lhs: &GitCommit{
RepoURL: "foo",
ID: "fake-commit-id",
},
rhs: &GitCommit{
RepoURL: "bar",
ID: "fake-commit-id",
},
expectedResult: false,
},
{
name: "commit IDs differ",
lhs: &GitCommit{
RepoURL: "fake-url",
ID: "foo",
},
rhs: &GitCommit{
RepoURL: "fake-url",
ID: "bar",
},
expectedResult: false,
},
{
name: "perfect match",
lhs: &GitCommit{
RepoURL: "fake-url",
ID: "fake-commit-id",
},
rhs: &GitCommit{
RepoURL: "fake-url",
ID: "fake-commit-id",
},
expectedResult: true,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
require.Equal(
t,
testCase.expectedResult,
testCase.lhs.Equals(testCase.rhs),
)
})
}
}

func TestFreightUpdateID(t *testing.T) {
freight := Freight{
Commits: []GitCommit{
{
RepoURL: "fake-git-repo",
ID: "fake-commit-id",
},
},
Images: []Image{
{
RepoURL: "fake-image-repo",
Tag: "fake-image-tag",
},
},
Charts: []Chart{
{
RegistryURL: "fake-chart-registry",
Name: "fake-chart",
Version: "fake-chart-version",
},
},
}
freight.UpdateID()
result := freight.ID
// Doing this any number of times should yield the same ID
for i := 0; i < 100; i++ {
freight.UpdateID()
require.Equal(t, result, freight.ID)
}
// Changing anything should change the result
freight.Commits[0].ID = "a-different-fake-commit"
freight.UpdateID()
require.NotEqual(t, result, freight.ID)
}
4 changes: 4 additions & 0 deletions api/v1alpha1/groupversion_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,16 @@ var (
// addKnownTypes adds the set of types defined in this package to the supplied scheme.
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(GroupVersion,
&Freight{},
&FreightList{},
&Stage{},
&StageList{},
&Promotion{},
&PromotionList{},
&PromotionPolicy{},
&PromotionPolicyList{},
&Warehouse{},
&WarehouseList{},
)
metav1.AddToGroupVersion(scheme, GroupVersion)
return nil
Expand Down
Loading

0 comments on commit 535d014

Please sign in to comment.