diff --git a/pkg/api/kptfile/v1/types.go b/pkg/api/kptfile/v1/types.go index c5bd88837f..4be3d7abd7 100644 --- a/pkg/api/kptfile/v1/types.go +++ b/pkg/api/kptfile/v1/types.go @@ -57,6 +57,8 @@ type KptFile struct { // Inventory contains parameters for the inventory object used in apply. Inventory *Inventory `yaml:"inventory,omitempty" json:"inventory,omitempty"` + + Status *Status `yaml:"status,omitempty" json:"status,omitempty"` } // OriginType defines the type of origin for a package. @@ -192,6 +194,12 @@ type PackageInfo struct { // Man is the path to documentation about the package Man string `yaml:"man,omitempty" json:"man,omitempty"` + + ReadinessGates []ReadinessGate `yaml:"readinessGates,omitempty" json:"readinessGates,omitempty"` +} + +type ReadinessGate struct { + ConditionType string `yaml:"conditionType" json:"conditionType"` } // Subpackages declares a local or remote subpackage. @@ -341,3 +349,25 @@ func (i Inventory) IsValid() bool { // Name, Namespace InventoryID are required inventory fields, so we check these 3 fields. return i.Name != "" && i.Namespace != "" && i.InventoryID != "" } + +type Status struct { + Conditions []Condition `yaml:"conditions,omitempty" json:"conditions,omitempty"` +} + +type Condition struct { + Type string `yaml:"type" json:"type"` + + Status ConditionStatus `yaml:"status" json:"status"` + + Reason string `yaml:"reason,omitempty" json:"reason,omitempty"` + + Message string `yaml:"message,omitempty" json:"message,omitempty"` +} + +type ConditionStatus string + +const ( + ConditionTrue ConditionStatus = "True" + ConditionFalse ConditionStatus = "False" + ConditionUnknown ConditionStatus = "Unknown" +) diff --git a/pkg/kptfile/kptfileutil/util.go b/pkg/kptfile/kptfileutil/util.go index b68f296405..a6fb9efacd 100644 --- a/pkg/kptfile/kptfileutil/util.go +++ b/pkg/kptfile/kptfileutil/util.go @@ -271,6 +271,7 @@ func merge(localKf, updatedKf, originalKf *kptfilev1.KptFile) error { localKf.Info = mergedKf.Info localKf.Pipeline = mergedKf.Pipeline localKf.Inventory = mergedKf.Inventory + localKf.Status = mergedKf.Status return nil } diff --git a/pkg/kptfile/kptfileutil/util_test.go b/pkg/kptfile/kptfileutil/util_test.go index 0dd153c607..9c0e21d88e 100644 --- a/pkg/kptfile/kptfileutil/util_test.go +++ b/pkg/kptfile/kptfileutil/util_test.go @@ -321,6 +321,254 @@ kind: Kptfile metadata: name: foo pipeline: {} +`, + }, + "first readinessGate and condition added in upstream": { + origin: ` +apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: foo +`, + updated: ` +apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: foo +info: + readinessGates: + - conditionType: foo +status: + conditions: + - type: foo + status: "True" + reason: reason + message: message +`, + local: ` +apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: foo +`, + updateUpstream: false, + expected: ` +apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: foo +info: + readinessGates: + - conditionType: foo +status: + conditions: + - type: foo + status: "True" + reason: reason + message: message +`, + }, + "additional readinessGate and condition added in upstream": { + origin: ` +apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: foo +info: + readinessGates: + - conditionType: foo +status: + conditions: + - type: foo + status: "True" + reason: reason + message: message +`, + updated: ` +apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: foo +info: + readinessGates: + - conditionType: foo + - conditionType: bar +status: + conditions: + - type: foo + status: "True" + reason: reason + message: message + - type: bar + status: "False" + reason: reason + message: message +`, + local: ` +apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: foo +info: + readinessGates: + - conditionType: foo +status: + conditions: + - type: foo + status: "True" + reason: reason + message: message +`, + updateUpstream: false, + expected: ` +apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: foo +info: + readinessGates: + - conditionType: foo + - conditionType: bar +status: + conditions: + - type: foo + status: "True" + reason: reason + message: message + - type: bar + status: "False" + reason: reason + message: message + `, + }, + "readinessGate added removed in upstream": { + origin: ` +apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: foo +info: + readinessGates: + - conditionType: foo +status: + conditions: + - type: foo + status: "True" + reason: reason + message: message +`, + updated: ` +apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: foo +`, + local: ` +apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: foo +info: + readinessGates: + - conditionType: foo +status: + conditions: + - type: foo + status: "True" + reason: reason + message: message +`, + updateUpstream: false, + expected: ` +apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: foo +info: {} +status: {} +`, + }, + "readinessGates removed and added in both upstream and local": { + origin: ` +apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: foo +info: + readinessGates: + - conditionType: foo + - conditionType: bar +status: + conditions: + - type: foo + status: "True" + reason: reason + message: message + - type: bar + status: "False" + reason: reason + message: message +`, + updated: ` +apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: foo +info: + readinessGates: + - conditionType: foo + - conditionType: zork +status: + conditions: + - type: foo + status: "True" + reason: reason + message: message + - type: zork + status: "Unknown" + reason: reason + message: message +`, + local: ` +apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: foo +info: + readinessGates: + - conditionType: xandar + - conditionType: foo +status: + conditions: + - type: xandar + status: "True" + reason: reason + message: message + - type: foo + status: "True" + reason: reason + message: message +`, + updateUpstream: false, + expected: ` +apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: foo +info: + readinessGates: + - conditionType: foo + - conditionType: zork +status: + conditions: + - type: foo + status: "True" + reason: reason + message: message + - type: zork + status: Unknown + reason: reason + message: message `, }, } diff --git a/porch/api/generated/clientset/versioned/fake/register.go b/porch/api/generated/clientset/versioned/fake/register.go index 2b1d2df776..36b9da3ca7 100644 --- a/porch/api/generated/clientset/versioned/fake/register.go +++ b/porch/api/generated/clientset/versioned/fake/register.go @@ -35,14 +35,14 @@ var localSchemeBuilder = runtime.SchemeBuilder{ // AddToScheme adds all types of this clientset into the given scheme. This allows composition // of clientsets, like in: // -// import ( -// "k8s.io/client-go/kubernetes" -// clientsetscheme "k8s.io/client-go/kubernetes/scheme" -// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" -// ) +// import ( +// "k8s.io/client-go/kubernetes" +// clientsetscheme "k8s.io/client-go/kubernetes/scheme" +// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" +// ) // -// kclientset, _ := kubernetes.NewForConfig(c) -// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) +// kclientset, _ := kubernetes.NewForConfig(c) +// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) // // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types // correctly. diff --git a/porch/api/generated/clientset/versioned/scheme/register.go b/porch/api/generated/clientset/versioned/scheme/register.go index 90fc1da236..cee00d25bb 100644 --- a/porch/api/generated/clientset/versioned/scheme/register.go +++ b/porch/api/generated/clientset/versioned/scheme/register.go @@ -35,14 +35,14 @@ var localSchemeBuilder = runtime.SchemeBuilder{ // AddToScheme adds all types of this clientset into the given scheme. This allows composition // of clientsets, like in: // -// import ( -// "k8s.io/client-go/kubernetes" -// clientsetscheme "k8s.io/client-go/kubernetes/scheme" -// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" -// ) +// import ( +// "k8s.io/client-go/kubernetes" +// clientsetscheme "k8s.io/client-go/kubernetes/scheme" +// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" +// ) // -// kclientset, _ := kubernetes.NewForConfig(c) -// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) +// kclientset, _ := kubernetes.NewForConfig(c) +// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) // // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types // correctly. diff --git a/porch/api/generated/openapi/zz_generated.openapi.go b/porch/api/generated/openapi/zz_generated.openapi.go index 0a0089e3e8..ecf07760bd 100644 --- a/porch/api/generated/openapi/zz_generated.openapi.go +++ b/porch/api/generated/openapi/zz_generated.openapi.go @@ -29,6 +29,7 @@ import ( func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { return map[string]common.OpenAPIDefinition{ + "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.Condition": schema_porch_api_porch_v1alpha1_Condition(ref), "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.Function": schema_porch_api_porch_v1alpha1_Function(ref), "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.FunctionConfig": schema_porch_api_porch_v1alpha1_FunctionConfig(ref), "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.FunctionEvalTaskSpec": schema_porch_api_porch_v1alpha1_FunctionEvalTaskSpec(ref), @@ -58,6 +59,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.PackageUpdateTaskSpec": schema_porch_api_porch_v1alpha1_PackageUpdateTaskSpec(ref), "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.ParentReference": schema_porch_api_porch_v1alpha1_ParentReference(ref), "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.PatchSpec": schema_porch_api_porch_v1alpha1_PatchSpec(ref), + "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.ReadinessGate": schema_porch_api_porch_v1alpha1_ReadinessGate(ref), "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.RepositoryRef": schema_porch_api_porch_v1alpha1_RepositoryRef(ref), "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.SecretRef": schema_porch_api_porch_v1alpha1_SecretRef(ref), "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.Selector": schema_porch_api_porch_v1alpha1_Selector(ref), @@ -119,6 +121,45 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA } } +func schema_porch_api_porch_v1alpha1_Condition(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "type": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "reason": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "message": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"type", "status"}, + }, + }, + } +} + func schema_porch_api_porch_v1alpha1_Function(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -1097,11 +1138,24 @@ func schema_porch_api_porch_v1alpha1_PackageRevisionSpec(ref common.ReferenceCal }, }, }, + "readinessGates": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.ReadinessGate"), + }, + }, + }, + }, + }, }, }, }, Dependencies: []string{ - "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.ParentReference", "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.Task"}, + "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.ParentReference", "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.ReadinessGate", "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.Task"}, } } @@ -1138,11 +1192,24 @@ func schema_porch_api_porch_v1alpha1_PackageRevisionStatus(ref common.ReferenceC Format: "", }, }, + "conditions": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.Condition"), + }, + }, + }, + }, + }, }, }, }, Dependencies: []string{ - "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.UpstreamLock", "k8s.io/apimachinery/pkg/apis/meta/v1.Time"}, + "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.Condition", "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.UpstreamLock", "k8s.io/apimachinery/pkg/apis/meta/v1.Time"}, } } @@ -1266,6 +1333,24 @@ func schema_porch_api_porch_v1alpha1_PatchSpec(ref common.ReferenceCallback) com } } +func schema_porch_api_porch_v1alpha1_ReadinessGate(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "conditionType": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + func schema_porch_api_porch_v1alpha1_RepositoryRef(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/porch/api/porch/types_packagerevisions.go b/porch/api/porch/types_packagerevisions.go index d9cc2ab4ef..48a4ecb2ec 100644 --- a/porch/api/porch/types_packagerevisions.go +++ b/porch/api/porch/types_packagerevisions.go @@ -66,6 +66,12 @@ type PackageRevisionSpec struct { Lifecycle PackageRevisionLifecycle `json:"lifecycle,omitempty"` Tasks []Task `json:"tasks,omitempty"` + + ReadinessGates []ReadinessGate `json:"readinessGates,omitempty"` +} + +type ReadinessGate struct { + ConditionType string `json:"conditionType,omitempty"` } // ParentReference is a reference to a parent package @@ -88,6 +94,8 @@ type PackageRevisionStatus struct { // Deployment is true if this is a deployment package (in a deployment repository). Deployment bool `json:"deployment,omitempty"` + + Conditions []Condition `json:"conditions,omitempty"` } type TaskType string @@ -302,3 +310,21 @@ type GitLock struct { // This is set by kpt for bookkeeping purposes. Commit string `yaml:"commit,omitempty" json:"commit,omitempty"` } + +type Condition struct { + Type string `yaml:"type" json:"type"` + + Status ConditionStatus `yaml:"type" json:"status"` + + Reason string `yaml:"type,omitempty" json:"reason,omitempty"` + + Message string `yaml:"type,omitempty" json:"message,omitempty"` +} + +type ConditionStatus string + +const ( + ConditionTrue ConditionStatus = "True" + ConditionFalse ConditionStatus = "False" + ConditionUnknown ConditionStatus = "Unknown" +) diff --git a/porch/api/porch/v1alpha1/types_packagerevisions.go b/porch/api/porch/v1alpha1/types_packagerevisions.go index b3f755fef7..6447bf49c2 100644 --- a/porch/api/porch/v1alpha1/types_packagerevisions.go +++ b/porch/api/porch/v1alpha1/types_packagerevisions.go @@ -74,6 +74,12 @@ type PackageRevisionSpec struct { Lifecycle PackageRevisionLifecycle `json:"lifecycle,omitempty"` Tasks []Task `json:"tasks,omitempty"` + + ReadinessGates []ReadinessGate `json:"readinessGates,omitempty"` +} + +type ReadinessGate struct { + ConditionType string `json:"conditionType,omitempty"` } // ParentReference is a reference to a parent package @@ -96,6 +102,8 @@ type PackageRevisionStatus struct { // Deployment is true if this is a deployment package (in a deployment repository). Deployment bool `json:"deployment,omitempty"` + + Conditions []Condition `json:"conditions,omitempty"` } type TaskType string @@ -313,3 +321,21 @@ type GitLock struct { // This is set by kpt for bookkeeping purposes. Commit string `yaml:"commit,omitempty" json:"commit,omitempty"` } + +type Condition struct { + Type string `yaml:"type" json:"type"` + + Status ConditionStatus `yaml:"status" json:"status"` + + Reason string `yaml:"reason,omitempty" json:"reason,omitempty"` + + Message string `yaml:"message,omitempty" json:"message,omitempty"` +} + +type ConditionStatus string + +const ( + ConditionTrue ConditionStatus = "True" + ConditionFalse ConditionStatus = "False" + ConditionUnknown ConditionStatus = "Unknown" +) diff --git a/porch/api/porch/v1alpha1/zz_generated.conversion.go b/porch/api/porch/v1alpha1/zz_generated.conversion.go index 4de340898b..b29424f493 100644 --- a/porch/api/porch/v1alpha1/zz_generated.conversion.go +++ b/porch/api/porch/v1alpha1/zz_generated.conversion.go @@ -34,6 +34,16 @@ func init() { // RegisterConversions adds conversion functions to the given scheme. // Public to allow building arbitrary schemes. func RegisterConversions(s *runtime.Scheme) error { + if err := s.AddGeneratedConversionFunc((*Condition)(nil), (*porch.Condition)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_Condition_To_porch_Condition(a.(*Condition), b.(*porch.Condition), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*porch.Condition)(nil), (*Condition)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_porch_Condition_To_v1alpha1_Condition(a.(*porch.Condition), b.(*Condition), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*Function)(nil), (*porch.Function)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha1_Function_To_porch_Function(a.(*Function), b.(*porch.Function), scope) }); err != nil { @@ -324,6 +334,16 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*ReadinessGate)(nil), (*porch.ReadinessGate)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_ReadinessGate_To_porch_ReadinessGate(a.(*ReadinessGate), b.(*porch.ReadinessGate), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*porch.ReadinessGate)(nil), (*ReadinessGate)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_porch_ReadinessGate_To_v1alpha1_ReadinessGate(a.(*porch.ReadinessGate), b.(*ReadinessGate), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*RepositoryRef)(nil), (*porch.RepositoryRef)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha1_RepositoryRef_To_porch_RepositoryRef(a.(*RepositoryRef), b.(*porch.RepositoryRef), scope) }); err != nil { @@ -387,6 +407,32 @@ func RegisterConversions(s *runtime.Scheme) error { return nil } +func autoConvert_v1alpha1_Condition_To_porch_Condition(in *Condition, out *porch.Condition, s conversion.Scope) error { + out.Type = in.Type + out.Status = porch.ConditionStatus(in.Status) + out.Reason = in.Reason + out.Message = in.Message + return nil +} + +// Convert_v1alpha1_Condition_To_porch_Condition is an autogenerated conversion function. +func Convert_v1alpha1_Condition_To_porch_Condition(in *Condition, out *porch.Condition, s conversion.Scope) error { + return autoConvert_v1alpha1_Condition_To_porch_Condition(in, out, s) +} + +func autoConvert_porch_Condition_To_v1alpha1_Condition(in *porch.Condition, out *Condition, s conversion.Scope) error { + out.Type = in.Type + out.Status = ConditionStatus(in.Status) + out.Reason = in.Reason + out.Message = in.Message + return nil +} + +// Convert_porch_Condition_To_v1alpha1_Condition is an autogenerated conversion function. +func Convert_porch_Condition_To_v1alpha1_Condition(in *porch.Condition, out *Condition, s conversion.Scope) error { + return autoConvert_porch_Condition_To_v1alpha1_Condition(in, out, s) +} + func autoConvert_v1alpha1_Function_To_porch_Function(in *Function, out *porch.Function, s conversion.Scope) error { out.ObjectMeta = in.ObjectMeta if err := Convert_v1alpha1_FunctionSpec_To_porch_FunctionSpec(&in.Spec, &out.Spec, s); err != nil { @@ -950,6 +996,7 @@ func autoConvert_v1alpha1_PackageRevisionSpec_To_porch_PackageRevisionSpec(in *P out.Parent = (*porch.ParentReference)(unsafe.Pointer(in.Parent)) out.Lifecycle = porch.PackageRevisionLifecycle(in.Lifecycle) out.Tasks = *(*[]porch.Task)(unsafe.Pointer(&in.Tasks)) + out.ReadinessGates = *(*[]porch.ReadinessGate)(unsafe.Pointer(&in.ReadinessGates)) return nil } @@ -965,6 +1012,7 @@ func autoConvert_porch_PackageRevisionSpec_To_v1alpha1_PackageRevisionSpec(in *p out.Parent = (*ParentReference)(unsafe.Pointer(in.Parent)) out.Lifecycle = PackageRevisionLifecycle(in.Lifecycle) out.Tasks = *(*[]Task)(unsafe.Pointer(&in.Tasks)) + out.ReadinessGates = *(*[]ReadinessGate)(unsafe.Pointer(&in.ReadinessGates)) return nil } @@ -978,6 +1026,7 @@ func autoConvert_v1alpha1_PackageRevisionStatus_To_porch_PackageRevisionStatus(i out.PublishedBy = in.PublishedBy out.PublishedAt = in.PublishedAt out.Deployment = in.Deployment + out.Conditions = *(*[]porch.Condition)(unsafe.Pointer(&in.Conditions)) return nil } @@ -991,6 +1040,7 @@ func autoConvert_porch_PackageRevisionStatus_To_v1alpha1_PackageRevisionStatus(i out.PublishedBy = in.PublishedBy out.PublishedAt = in.PublishedAt out.Deployment = in.Deployment + out.Conditions = *(*[]Condition)(unsafe.Pointer(&in.Conditions)) return nil } @@ -1109,6 +1159,26 @@ func Convert_porch_PatchSpec_To_v1alpha1_PatchSpec(in *porch.PatchSpec, out *Pat return autoConvert_porch_PatchSpec_To_v1alpha1_PatchSpec(in, out, s) } +func autoConvert_v1alpha1_ReadinessGate_To_porch_ReadinessGate(in *ReadinessGate, out *porch.ReadinessGate, s conversion.Scope) error { + out.ConditionType = in.ConditionType + return nil +} + +// Convert_v1alpha1_ReadinessGate_To_porch_ReadinessGate is an autogenerated conversion function. +func Convert_v1alpha1_ReadinessGate_To_porch_ReadinessGate(in *ReadinessGate, out *porch.ReadinessGate, s conversion.Scope) error { + return autoConvert_v1alpha1_ReadinessGate_To_porch_ReadinessGate(in, out, s) +} + +func autoConvert_porch_ReadinessGate_To_v1alpha1_ReadinessGate(in *porch.ReadinessGate, out *ReadinessGate, s conversion.Scope) error { + out.ConditionType = in.ConditionType + return nil +} + +// Convert_porch_ReadinessGate_To_v1alpha1_ReadinessGate is an autogenerated conversion function. +func Convert_porch_ReadinessGate_To_v1alpha1_ReadinessGate(in *porch.ReadinessGate, out *ReadinessGate, s conversion.Scope) error { + return autoConvert_porch_ReadinessGate_To_v1alpha1_ReadinessGate(in, out, s) +} + func autoConvert_v1alpha1_RepositoryRef_To_porch_RepositoryRef(in *RepositoryRef, out *porch.RepositoryRef, s conversion.Scope) error { out.Name = in.Name return nil diff --git a/porch/api/porch/v1alpha1/zz_generated.deepcopy.go b/porch/api/porch/v1alpha1/zz_generated.deepcopy.go index 24cbb1ab4a..6b7440b6d2 100644 --- a/porch/api/porch/v1alpha1/zz_generated.deepcopy.go +++ b/porch/api/porch/v1alpha1/zz_generated.deepcopy.go @@ -23,6 +23,22 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Condition) DeepCopyInto(out *Condition) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition. +func (in *Condition) DeepCopy() *Condition { + if in == nil { + return nil + } + out := new(Condition) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Function) DeepCopyInto(out *Function) { *out = *in @@ -567,6 +583,11 @@ func (in *PackageRevisionSpec) DeepCopyInto(out *PackageRevisionSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.ReadinessGates != nil { + in, out := &in.ReadinessGates, &out.ReadinessGates + *out = make([]ReadinessGate, len(*in)) + copy(*out, *in) + } return } @@ -589,6 +610,11 @@ func (in *PackageRevisionStatus) DeepCopyInto(out *PackageRevisionStatus) { (*in).DeepCopyInto(*out) } in.PublishedAt.DeepCopyInto(&out.PublishedAt) + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + copy(*out, *in) + } return } @@ -683,6 +709,22 @@ func (in *PatchSpec) DeepCopy() *PatchSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ReadinessGate) DeepCopyInto(out *ReadinessGate) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReadinessGate. +func (in *ReadinessGate) DeepCopy() *ReadinessGate { + if in == nil { + return nil + } + out := new(ReadinessGate) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RepositoryRef) DeepCopyInto(out *RepositoryRef) { *out = *in diff --git a/porch/api/porch/zz_generated.deepcopy.go b/porch/api/porch/zz_generated.deepcopy.go index cd31543f08..c66dd4ddae 100644 --- a/porch/api/porch/zz_generated.deepcopy.go +++ b/porch/api/porch/zz_generated.deepcopy.go @@ -23,6 +23,22 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Condition) DeepCopyInto(out *Condition) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition. +func (in *Condition) DeepCopy() *Condition { + if in == nil { + return nil + } + out := new(Condition) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Function) DeepCopyInto(out *Function) { *out = *in @@ -567,6 +583,11 @@ func (in *PackageRevisionSpec) DeepCopyInto(out *PackageRevisionSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.ReadinessGates != nil { + in, out := &in.ReadinessGates, &out.ReadinessGates + *out = make([]ReadinessGate, len(*in)) + copy(*out, *in) + } return } @@ -589,6 +610,11 @@ func (in *PackageRevisionStatus) DeepCopyInto(out *PackageRevisionStatus) { (*in).DeepCopyInto(*out) } in.PublishedAt.DeepCopyInto(&out.PublishedAt) + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + copy(*out, *in) + } return } @@ -683,6 +709,22 @@ func (in *PatchSpec) DeepCopy() *PatchSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ReadinessGate) DeepCopyInto(out *ReadinessGate) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReadinessGate. +func (in *ReadinessGate) DeepCopy() *ReadinessGate { + if in == nil { + return nil + } + out := new(ReadinessGate) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RepositoryRef) DeepCopyInto(out *RepositoryRef) { *out = *in diff --git a/porch/pkg/cache/cache_test.go b/porch/pkg/cache/cache_test.go index d1b434c7ec..7ae9e035d7 100644 --- a/porch/pkg/cache/cache_test.go +++ b/porch/pkg/cache/cache_test.go @@ -47,7 +47,7 @@ func TestLatestPackages(t *testing.T) { gotLatest := map[string]string{} for _, pr := range revisions { - rev := pr.GetPackageRevision() + rev := pr.GetPackageRevision(ctx) if latest, ok := rev.Labels[api.LatestPackageRevisionKey]; ok { if got, want := latest, api.LatestPackageRevisionValue; got != want { @@ -105,7 +105,7 @@ func TestPublishedLatest(t *testing.T) { if err != nil { t.Fatalf("Close failed: %v", err) } - resource := closed.GetPackageRevision() + resource := closed.GetPackageRevision(ctx) if got, ok := resource.Labels[api.LatestPackageRevisionKey]; !ok { t.Errorf("Label %s not found as expected", api.LatestPackageRevisionKey) } else if want := api.LatestPackageRevisionValue; got != want { diff --git a/porch/pkg/cache/packagerevision.go b/porch/pkg/cache/packagerevision.go index a89e14d6fd..2d624da2e1 100644 --- a/porch/pkg/cache/packagerevision.go +++ b/porch/pkg/cache/packagerevision.go @@ -15,6 +15,8 @@ package cache import ( + "context" + "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1" "github.com/GoogleContainerTools/kpt/porch/pkg/repository" ) @@ -32,8 +34,8 @@ type cachedPackageRevision struct { isLatestRevision bool } -func (c *cachedPackageRevision) GetPackageRevision() *v1alpha1.PackageRevision { - rev := c.PackageRevision.GetPackageRevision() +func (c *cachedPackageRevision) GetPackageRevision(ctx context.Context) *v1alpha1.PackageRevision { + rev := c.PackageRevision.GetPackageRevision(ctx) if c.isLatestRevision { if rev.Labels == nil { rev.Labels = map[string]string{} diff --git a/porch/pkg/engine/engine.go b/porch/pkg/engine/engine.go index 4669bbd1d5..aa1e1e6983 100644 --- a/porch/pkg/engine/engine.go +++ b/porch/pkg/engine/engine.go @@ -15,6 +15,7 @@ package engine import ( + "bytes" "context" "fmt" "io/fs" @@ -22,6 +23,7 @@ import ( "path/filepath" "reflect" + kptfile "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1" "github.com/GoogleContainerTools/kpt/pkg/fn" api "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1" configapi "github.com/GoogleContainerTools/kpt/porch/api/porchconfig/v1alpha1" @@ -328,14 +330,28 @@ func (cad *cadEngine) UpdatePackageRevision(ctx context.Context, repositoryObj * mutations = append(mutations, mutation) } - // Re-render if we are making changes. - mutations = cad.conditionalAddRender(mutations) - draft, err := repo.UpdatePackageRevision(ctx, oldPackage) if err != nil { return nil, err } + // If any of the fields in the API that are projections from the Kptfile + // must be updated in the Kptfile as well. + kfPatchTask, created, err := createKptfilePatchTask(ctx, oldPackage, newObj) + if err != nil { + return nil, err + } + if created { + kfPatchMutation, err := buildPatchMutation(ctx, kfPatchTask) + if err != nil { + return nil, err + } + mutations = append(mutations, kfPatchMutation) + } + + // Re-render if we are making changes. + mutations = cad.conditionalAddRender(mutations) + // TODO: Handle the case if alongside lifecycle change, tasks are changed too. // Update package contents only if the package is in draft state if oldObj.Spec.Lifecycle == api.PackageRevisionLifecycleDraft { @@ -360,6 +376,95 @@ func (cad *cadEngine) UpdatePackageRevision(ctx context.Context, repositoryObj * return draft.Close(ctx) } +func createKptfilePatchTask(ctx context.Context, oldPackage repository.PackageRevision, newObj *api.PackageRevision) (*api.Task, bool, error) { + kf, err := oldPackage.GetKptfile(ctx) + if err != nil { + return nil, false, err + } + + var orgKfString string + { + var buf bytes.Buffer + d := yaml.NewEncoder(&buf) + if err := d.Encode(kf); err != nil { + return nil, false, err + } + orgKfString = buf.String() + } + + var readinessGates []kptfile.ReadinessGate + for _, rg := range newObj.Spec.ReadinessGates { + readinessGates = append(readinessGates, kptfile.ReadinessGate{ + ConditionType: rg.ConditionType, + }) + } + + var conditions []kptfile.Condition + for _, c := range newObj.Status.Conditions { + conditions = append(conditions, kptfile.Condition{ + Type: c.Type, + Status: convertStatusToKptfile(c.Status), + Reason: c.Reason, + Message: c.Message, + }) + } + + if kf.Info == nil && len(readinessGates) > 0 { + kf.Info = &kptfile.PackageInfo{} + } + if len(readinessGates) > 0 { + kf.Info.ReadinessGates = readinessGates + } + + if kf.Status == nil && len(conditions) > 0 { + kf.Status = &kptfile.Status{} + } + if len(conditions) > 0 { + kf.Status.Conditions = conditions + } + + var newKfString string + { + var buf bytes.Buffer + d := yaml.NewEncoder(&buf) + if err := d.Encode(kf); err != nil { + return nil, false, err + } + newKfString = buf.String() + } + + patchSpec, err := GeneratePatch(kptfile.KptFileName, orgKfString, newKfString) + if err != nil { + return nil, false, err + } + // If patch is empty, don't create a Task. + if patchSpec.Contents == "" { + return nil, false, nil + } + + return &api.Task{ + Type: api.TaskTypePatch, + Patch: &api.PackagePatchTaskSpec{ + Patches: []api.PatchSpec{ + patchSpec, + }, + }, + }, true, nil +} + +func convertStatusToKptfile(s api.ConditionStatus) kptfile.ConditionStatus { + switch s { + case api.ConditionTrue: + return kptfile.ConditionTrue + case api.ConditionFalse: + return kptfile.ConditionFalse + case api.ConditionUnknown: + return kptfile.ConditionUnknown + default: + panic(fmt.Errorf("unknown condition status: %v", s)) + } +} + // conditionalAddRender adds a render mutation to the end of the mutations slice if the last // entry is not already a render mutation. func (cad *cadEngine) conditionalAddRender(mutations []mutation) []mutation { @@ -452,7 +557,7 @@ func (cad *cadEngine) UpdatePackageResources(ctx context.Context, repositoryObj ctx, span := tracer.Start(ctx, "cadEngine::UpdatePackageResources", trace.WithAttributes()) defer span.End() - rev := oldPackage.GetPackageRevision() + rev := oldPackage.GetPackageRevision(ctx) // Validate package lifecycle. Can only update a draft. switch lifecycle := rev.Spec.Lifecycle; lifecycle { diff --git a/porch/pkg/engine/engine_test.go b/porch/pkg/engine/engine_test.go new file mode 100644 index 0000000000..d40cc15332 --- /dev/null +++ b/porch/pkg/engine/engine_test.go @@ -0,0 +1,317 @@ +// Copyright 2022 Google LLC +// +// 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 engine + +import ( + "context" + "strings" + "testing" + + kptfile "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1" + api "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1" + "github.com/GoogleContainerTools/kpt/porch/pkg/engine/fake" + "github.com/GoogleContainerTools/kpt/porch/pkg/repository" + "github.com/google/go-cmp/cmp" +) + +func TestSomething(t *testing.T) { + testCases := map[string]struct { + repoPkgRev repository.PackageRevision + newApiPkgRev *api.PackageRevision + hasPatch bool + patch api.PatchSpec + }{ + "no gates or conditions": { + repoPkgRev: &fake.PackageRevision{ + Kptfile: kptfile.KptFile{}, + }, + newApiPkgRev: &api.PackageRevision{ + Spec: api.PackageRevisionSpec{}, + }, + hasPatch: false, + }, + "first gate and condition added": { + repoPkgRev: &fake.PackageRevision{ + Kptfile: kptfile.KptFile{}, + }, + newApiPkgRev: &api.PackageRevision{ + Spec: api.PackageRevisionSpec{ + ReadinessGates: []api.ReadinessGate{ + { + ConditionType: "foo", + }, + }, + }, + Status: api.PackageRevisionStatus{ + Conditions: []api.Condition{ + { + Type: "foo", + Status: api.ConditionTrue, + }, + }, + }, + }, + hasPatch: true, + patch: api.PatchSpec{ + File: kptfile.KptFileName, + Contents: strings.TrimSpace(` +--- Kptfile ++++ Kptfile +@@ -1 +1,7 @@ +-{} ++info: ++ readinessGates: ++ - conditionType: foo ++status: ++ conditions: ++ - type: foo ++ status: "True" +`) + "\n", + PatchType: api.PatchTypePatchFile, + }, + }, + "additional readinessGates and conditions added": { + repoPkgRev: &fake.PackageRevision{ + Kptfile: kptfile.KptFile{ + Info: &kptfile.PackageInfo{ + ReadinessGates: []kptfile.ReadinessGate{ + { + ConditionType: "foo", + }, + }, + }, + Status: &kptfile.Status{ + Conditions: []kptfile.Condition{ + { + Type: "foo", + Status: kptfile.ConditionTrue, + }, + }, + }, + }, + }, + newApiPkgRev: &api.PackageRevision{ + Spec: api.PackageRevisionSpec{ + ReadinessGates: []api.ReadinessGate{ + { + ConditionType: "foo", + }, + { + ConditionType: "bar", + }, + }, + }, + Status: api.PackageRevisionStatus{ + Conditions: []api.Condition{ + { + Type: "foo", + Status: api.ConditionTrue, + Reason: "reason", + Message: "message", + }, + { + Type: "bar", + Status: api.ConditionFalse, + Reason: "reason", + Message: "message", + }, + }, + }, + }, + hasPatch: true, + patch: api.PatchSpec{ + File: kptfile.KptFileName, + Contents: strings.TrimSpace(` +--- Kptfile ++++ Kptfile +@@ -1,7 +1,14 @@ + info: + readinessGates: + - conditionType: foo ++ - conditionType: bar + status: + conditions: + - type: foo + status: "True" ++ reason: reason ++ message: message ++ - type: bar ++ status: "False" ++ reason: reason ++ message: message +`) + "\n", + PatchType: api.PatchTypePatchFile, + }, + }, + "no changes": { + repoPkgRev: &fake.PackageRevision{ + Kptfile: kptfile.KptFile{ + Info: &kptfile.PackageInfo{ + ReadinessGates: []kptfile.ReadinessGate{ + { + ConditionType: "foo", + }, + { + ConditionType: "bar", + }, + }, + }, + Status: &kptfile.Status{ + Conditions: []kptfile.Condition{ + { + Type: "foo", + Status: kptfile.ConditionTrue, + Reason: "reason", + Message: "message", + }, + { + Type: "bar", + Status: kptfile.ConditionFalse, + Reason: "reason", + Message: "message", + }, + }, + }, + }, + }, + newApiPkgRev: &api.PackageRevision{ + Spec: api.PackageRevisionSpec{ + ReadinessGates: []api.ReadinessGate{ + { + ConditionType: "foo", + }, + { + ConditionType: "bar", + }, + }, + }, + Status: api.PackageRevisionStatus{ + Conditions: []api.Condition{ + { + Type: "foo", + Status: api.ConditionTrue, + Reason: "reason", + Message: "message", + }, + { + Type: "bar", + Status: api.ConditionFalse, + Reason: "reason", + Message: "message", + }, + }, + }, + }, + hasPatch: false, + }, + "readinessGates and conditions removed": { + repoPkgRev: &fake.PackageRevision{ + Kptfile: kptfile.KptFile{ + Info: &kptfile.PackageInfo{ + ReadinessGates: []kptfile.ReadinessGate{ + { + ConditionType: "foo", + }, + { + ConditionType: "bar", + }, + }, + }, + Status: &kptfile.Status{ + Conditions: []kptfile.Condition{ + { + Type: "foo", + Status: kptfile.ConditionTrue, + Reason: "reason", + Message: "message", + }, + { + Type: "bar", + Status: kptfile.ConditionFalse, + Reason: "reason", + Message: "message", + }, + }, + }, + }, + }, + newApiPkgRev: &api.PackageRevision{ + Spec: api.PackageRevisionSpec{ + ReadinessGates: []api.ReadinessGate{ + { + ConditionType: "foo", + }, + }, + }, + Status: api.PackageRevisionStatus{ + Conditions: []api.Condition{ + { + Type: "foo", + Status: api.ConditionTrue, + }, + }, + }, + }, + hasPatch: true, + patch: api.PatchSpec{ + File: kptfile.KptFileName, + Contents: strings.TrimSpace(` +--- Kptfile ++++ Kptfile +@@ -1,14 +1,7 @@ + info: + readinessGates: + - conditionType: foo +- - conditionType: bar + status: + conditions: + - type: foo + status: "True" +- reason: reason +- message: message +- - type: bar +- status: "False" +- reason: reason +- message: message +`) + "\n", + PatchType: api.PatchTypePatchFile, + }, + }, + } + + for tn := range testCases { + tc := testCases[tn] + t.Run(tn, func(t *testing.T) { + task, hasPatch, err := createKptfilePatchTask(context.Background(), tc.repoPkgRev, tc.newApiPkgRev) + if err != nil { + t.Fatal(err) + } + + if tc.hasPatch && !hasPatch { + t.Errorf("expected patch, but didn't get one") + } + if !tc.hasPatch { + if hasPatch { + t.Errorf("expected no patch, but got one") + } + return + } + + if diff := cmp.Diff(tc.patch, task.Patch.Patches[0]); diff != "" { + t.Errorf("Unexpected result (-want, +got): %s", diff) + } + }) + } +} diff --git a/porch/pkg/engine/fake/packagerevision.go b/porch/pkg/engine/fake/packagerevision.go index e87088513c..04c2f6d991 100644 --- a/porch/pkg/engine/fake/packagerevision.go +++ b/porch/pkg/engine/fake/packagerevision.go @@ -29,8 +29,7 @@ type PackageRevision struct { PackageLifecycle v1alpha1.PackageRevisionLifecycle PackageRevision *v1alpha1.PackageRevision Resources *v1alpha1.PackageRevisionResources - Upstream kptfile.Upstream - UpstreamLock kptfile.UpstreamLock + Kptfile kptfile.KptFile } func (pr *PackageRevision) KubeObjectName() string { @@ -45,7 +44,7 @@ func (pr *PackageRevision) Lifecycle() v1alpha1.PackageRevisionLifecycle { return pr.PackageLifecycle } -func (pr *PackageRevision) GetPackageRevision() *v1alpha1.PackageRevision { +func (pr *PackageRevision) GetPackageRevision(context.Context) *v1alpha1.PackageRevision { return nil } @@ -53,10 +52,14 @@ func (f *PackageRevision) GetResources(context.Context) (*v1alpha1.PackageRevisi return f.Resources, nil } -func (f *PackageRevision) GetUpstreamLock() (kptfile.Upstream, kptfile.UpstreamLock, error) { - return kptfile.Upstream{}, kptfile.UpstreamLock{}, nil +func (f *PackageRevision) GetKptfile(ctx context.Context) (kptfile.KptFile, error) { + return f.Kptfile, nil +} + +func (f *PackageRevision) GetUpstreamLock(context.Context) (kptfile.Upstream, kptfile.UpstreamLock, error) { + return *f.Kptfile.Upstream, *f.Kptfile.UpstreamLock, nil } func (f *PackageRevision) GetLock() (kptfile.Upstream, kptfile.UpstreamLock, error) { - return kptfile.Upstream{}, kptfile.UpstreamLock{}, nil + return *f.Kptfile.Upstream, *f.Kptfile.UpstreamLock, nil } diff --git a/porch/pkg/git/git.go b/porch/pkg/git/git.go index aaaf38ab45..0f79f26340 100644 --- a/porch/pkg/git/git.go +++ b/porch/pkg/git/git.go @@ -962,6 +962,35 @@ func (r *gitRepository) loadTasks(ctx context.Context, startCommit *object.Commi return tasks, nil } +func (r *gitRepository) getResources(hash plumbing.Hash) (map[string]string, error) { + resources := map[string]string{} + + tree, err := r.repo.TreeObject(hash) + if err == nil { + // Files() iterator iterates recursively over all files in the tree. + fit := tree.Files() + defer fit.Close() + for { + file, err := fit.Next() + if err == io.EOF { + break + } else if err != nil { + return nil, fmt.Errorf("failed to load package resources: %w", err) + } + + content, err := file.Contents() + if err != nil { + return nil, fmt.Errorf("failed to read package file contents: %q, %w", file.Name, err) + } + + // TODO: decide whether paths should include package directory or not. + resources[file.Name] = content + //resources[path.Join(p.path, file.Name)] = content + } + } + return resources, nil +} + // findLatestPackageCommit returns the latest commit from the history that pertains // to the package given by the packagePath. If no commit is found, it will return nil. func (r *gitRepository) findLatestPackageCommit(ctx context.Context, startCommit *object.Commit, packagePath string) (*object.Commit, error) { diff --git a/porch/pkg/git/git_test.go b/porch/pkg/git/git_test.go index 7103b935f9..06d3e6ceae 100644 --- a/porch/pkg/git/git_test.go +++ b/porch/pkg/git/git_test.go @@ -419,7 +419,7 @@ func (g GitSuite) TestListPackagesTrivial(t *testing.T) { t.Fatalf("draft.Close() failed: %v", err) } - result := newRevision.GetPackageRevision() + result := newRevision.GetPackageRevision(ctx) if got, want := result.Spec.Lifecycle, v1alpha1.PackageRevisionLifecycleDraft; got != want { t.Errorf("Newly created package type: got %q, want %q", got, want) } @@ -504,7 +504,7 @@ func (g GitSuite) TestCreatePackageInTrivialRepository(t *testing.T) { t.Fatalf("draft.Close() failed: %v", err) } - result := newRevision.GetPackageRevision() + result := newRevision.GetPackageRevision(ctx) if got, want := result.Spec.Lifecycle, v1alpha1.PackageRevisionLifecycleDraft; got != want { t.Errorf("Newly created package type: got %q, want %q", got, want) } @@ -554,7 +554,7 @@ func (g GitSuite) TestListPackagesSimple(t *testing.T) { got := map[repository.PackageRevisionKey]v1alpha1.PackageRevisionLifecycle{} for _, r := range revisions { - rev := r.GetPackageRevision() + rev := r.GetPackageRevision(ctx) got[repository.PackageRevisionKey{ Repository: rev.Spec.RepositoryName, Package: rev.Spec.PackageName, @@ -613,7 +613,7 @@ func (g GitSuite) TestListPackagesDrafts(t *testing.T) { got := map[repository.PackageRevisionKey]v1alpha1.PackageRevisionLifecycle{} for _, r := range revisions { - rev := r.GetPackageRevision() + rev := r.GetPackageRevision(ctx) got[repository.PackageRevisionKey{ Repository: rev.Spec.RepositoryName, Package: rev.Spec.PackageName, @@ -675,7 +675,7 @@ func (g GitSuite) TestApproveDraft(t *testing.T) { t.Fatalf("Close failed: %v", err) } - rev := new.GetPackageRevision() + rev := new.GetPackageRevision(ctx) if got, want := rev.Spec.Lifecycle, v1alpha1.PackageRevisionLifecyclePublished; got != want { t.Errorf("Approved package lifecycle: got %s, want %s", got, want) } @@ -734,7 +734,7 @@ func (g GitSuite) TestApproveDraftWithHistory(t *testing.T) { t.Fatalf("Close failed: %v", err) } - rev := new.GetPackageRevision() + rev := new.GetPackageRevision(ctx) if got, want := rev.Spec.Lifecycle, v1alpha1.PackageRevisionLifecyclePublished; got != want { t.Errorf("Approved package lifecycle: got %s, want %s", got, want) } @@ -787,7 +787,7 @@ func (g GitSuite) TestDeletePackages(t *testing.T) { for len(all) > 0 { // Delete one of the packages deleting := all[0] - pr := deleting.GetPackageRevision() + pr := deleting.GetPackageRevision(ctx) name := repository.PackageRevisionKey{Repository: pr.Spec.RepositoryName, Package: pr.Spec.PackageName, Revision: pr.Spec.Revision} if rn, ok := wantDeletedRefs[name]; ok { @@ -1050,7 +1050,7 @@ func (g GitSuite) TestNested(t *testing.T) { got := map[string]v1alpha1.PackageRevisionLifecycle{} for _, pr := range revisions { - rev := pr.GetPackageRevision() + rev := pr.GetPackageRevision(ctx) if rev.Spec.Revision == g.branch { // skip packages with the revision of the main registered branch, // to match the above simplified package discovery algo. @@ -1209,7 +1209,7 @@ func (g GitSuite) TestAuthor(t *testing.T) { Package: tc.pkg, Revision: tc.revision, }) - rev := draftPkg.GetPackageRevision() + rev := draftPkg.GetPackageRevision(ctx) if got, want := rev.Status.PublishedBy, tc.author; got != want { t.Errorf("expected %q, but got %q", want, got) } diff --git a/porch/pkg/git/package.go b/porch/pkg/git/package.go index dbd0d375c9..e6c0c55477 100644 --- a/porch/pkg/git/package.go +++ b/porch/pkg/git/package.go @@ -19,7 +19,6 @@ import ( "crypto/sha1" "encoding/hex" "fmt" - "io" "strings" "time" @@ -71,10 +70,10 @@ func (p *gitPackageRevision) uid() types.UID { return types.UID(fmt.Sprintf("uid:%s:%s", p.path, p.revision)) } -func (p *gitPackageRevision) GetPackageRevision() *v1alpha1.PackageRevision { +func (p *gitPackageRevision) GetPackageRevision(ctx context.Context) *v1alpha1.PackageRevision { key := p.Key() - _, lock, _ := p.GetUpstreamLock() + _, lock, _ := p.GetUpstreamLock(ctx) lockCopy := &v1alpha1.UpstreamLock{} // TODO: Use kpt definition of UpstreamLock in the package revision status @@ -92,10 +91,13 @@ func (p *gitPackageRevision) GetPackageRevision() *v1alpha1.PackageRevision { } } + kf, _ := p.GetKptfile(ctx) + status := v1alpha1.PackageRevisionStatus{ UpstreamLock: lockCopy, + Deployment: p.repo.deployment, + Conditions: repository.ToApiConditions(kf), } - status.Deployment = p.repo.deployment if p.Lifecycle() == v1alpha1.PackageRevisionLifecyclePublished { if !p.updated.IsZero() { @@ -125,38 +127,18 @@ func (p *gitPackageRevision) GetPackageRevision() *v1alpha1.PackageRevision { Revision: key.Revision, RepositoryName: key.Repository, - Lifecycle: p.Lifecycle(), - Tasks: p.tasks, + Lifecycle: p.Lifecycle(), + Tasks: p.tasks, + ReadinessGates: repository.ToApiReadinessGates(kf), }, Status: status, } } func (p *gitPackageRevision) GetResources(ctx context.Context) (*v1alpha1.PackageRevisionResources, error) { - resources := map[string]string{} - - tree, err := p.repo.repo.TreeObject(p.tree) - if err == nil { - // Files() iterator iterates recursively over all files in the tree. - fit := tree.Files() - defer fit.Close() - for { - file, err := fit.Next() - if err == io.EOF { - break - } else if err != nil { - return nil, fmt.Errorf("failed to load package resources: %w", err) - } - - content, err := file.Contents() - if err != nil { - return nil, fmt.Errorf("failed to read package file contents: %q, %w", file.Name, err) - } - - // TODO: decide whether paths should include package directory or not. - resources[file.Name] = content - //resources[path.Join(p.path, file.Name)] = content - } + resources, err := p.repo.getResources(p.tree) + if err != nil { + return nil, fmt.Errorf("failed to load package resources: %w", err) } key := p.Key() @@ -186,20 +168,27 @@ func (p *gitPackageRevision) GetResources(ctx context.Context) (*v1alpha1.Packag }, nil } -// GetUpstreamLock returns the upstreamLock info present in the Kptfile of the package. -func (p *gitPackageRevision) GetUpstreamLock() (kptfile.Upstream, kptfile.UpstreamLock, error) { - resources, err := p.GetResources(context.Background()) +func (p *gitPackageRevision) GetKptfile(ctx context.Context) (kptfile.KptFile, error) { + resources, err := p.repo.getResources(p.tree) if err != nil { - return kptfile.Upstream{}, kptfile.UpstreamLock{}, fmt.Errorf("cannot determine package lock; cannot retrieve resources: %w", err) + return kptfile.KptFile{}, fmt.Errorf("error loading package resources: %w", err) } + kfString, found := resources[kptfile.KptFileName] + if !found { + return kptfile.KptFile{}, fmt.Errorf("packagerevision does not have a Kptfile") + } + kf, err := pkg.DecodeKptfile(strings.NewReader(kfString)) + if err != nil { + return kptfile.KptFile{}, fmt.Errorf("error decoding Kptfile: %w", err) + } + return *kf, nil +} - // get the upstream package URL from the Kptfile - var kf *kptfile.KptFile - if contents, found := resources.Spec.Resources[kptfile.KptFileName]; found { - kf, err = pkg.DecodeKptfile(strings.NewReader(contents)) - if err != nil { - return kptfile.Upstream{}, kptfile.UpstreamLock{}, fmt.Errorf("cannot decode Kptfile: %w", err) - } +// GetUpstreamLock returns the upstreamLock info present in the Kptfile of the package. +func (p *gitPackageRevision) GetUpstreamLock(ctx context.Context) (kptfile.Upstream, kptfile.UpstreamLock, error) { + kf, err := p.GetKptfile(ctx) + if err != nil { + return kptfile.Upstream{}, kptfile.UpstreamLock{}, fmt.Errorf("cannot determine package lock; cannot retrieve resources: %w", err) } if kf.Upstream == nil || kf.UpstreamLock == nil || kf.Upstream.Git == nil { diff --git a/porch/pkg/oci/oci.go b/porch/pkg/oci/oci.go index 2232bff39c..1e3221e259 100644 --- a/porch/pkg/oci/oci.go +++ b/porch/pkg/oci/oci.go @@ -19,8 +19,10 @@ import ( "crypto/sha1" "encoding/hex" "fmt" + "strings" "time" + "github.com/GoogleContainerTools/kpt/internal/pkg" kptfile "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1" "github.com/GoogleContainerTools/kpt/pkg/oci" "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1" @@ -379,9 +381,11 @@ func (p *ociPackageRevision) Key() repository.PackageRevisionKey { } } -func (p *ociPackageRevision) GetPackageRevision() *v1alpha1.PackageRevision { +func (p *ociPackageRevision) GetPackageRevision(ctx context.Context) *v1alpha1.PackageRevision { key := p.Key() + kf, _ := p.GetKptfile(ctx) + return &v1alpha1.PackageRevision{ TypeMeta: metav1.TypeMeta{ Kind: "PackageRevision", @@ -401,17 +405,35 @@ func (p *ociPackageRevision) GetPackageRevision() *v1alpha1.PackageRevision { Revision: key.Revision, RepositoryName: key.Repository, - Lifecycle: p.Lifecycle(), - Tasks: p.tasks, + Lifecycle: p.Lifecycle(), + Tasks: p.tasks, + ReadinessGates: repository.ToApiReadinessGates(kf), }, Status: v1alpha1.PackageRevisionStatus{ // TODO: UpstreamLock, Deployment: p.parent.deployment, + Conditions: repository.ToApiConditions(kf), }, } } -func (p *ociPackageRevision) GetUpstreamLock() (kptfile.Upstream, kptfile.UpstreamLock, error) { +func (p *ociPackageRevision) GetKptfile(ctx context.Context) (kptfile.KptFile, error) { + resources, err := LoadResources(ctx, p.parent.storage, &p.digestName) + if err != nil { + return kptfile.KptFile{}, fmt.Errorf("error loading package resources: %w", err) + } + kfString, found := resources.Contents[kptfile.KptFileName] + if !found { + return kptfile.KptFile{}, fmt.Errorf("packagerevision does not have a Kptfile") + } + kf, err := pkg.DecodeKptfile(strings.NewReader(kfString)) + if err != nil { + return kptfile.KptFile{}, fmt.Errorf("error decoding Kptfile: %w", err) + } + return *kf, nil +} + +func (p *ociPackageRevision) GetUpstreamLock(context.Context) (kptfile.Upstream, kptfile.UpstreamLock, error) { return kptfile.Upstream{}, kptfile.UpstreamLock{}, fmt.Errorf("UpstreamLock is not supported for OCI packages (%s)", p.KubeObjectName()) } diff --git a/porch/pkg/registry/porch/packagecommon.go b/porch/pkg/registry/porch/packagecommon.go index d96052cacf..d29a705c01 100644 --- a/porch/pkg/registry/porch/packagecommon.go +++ b/porch/pkg/registry/porch/packagecommon.go @@ -212,7 +212,7 @@ func (r *packageCommon) updatePackageRevision(ctx context.Context, name string, var oldRuntimeObj runtime.Object // We have to be runtime.Object (and not *api.PackageRevision) or else nil-checks fail (because a nil object is not a nil interface) if !isCreate { - oldRuntimeObj = oldPackage.GetPackageRevision() + oldRuntimeObj = oldPackage.GetPackageRevision(ctx) } newRuntimeObj, err := objInfo.UpdatedObject(ctx, oldRuntimeObj) @@ -268,7 +268,7 @@ func (r *packageCommon) updatePackageRevision(ctx context.Context, name string, return nil, false, apierrors.NewInternalError(err) } - updated := rev.GetPackageRevision() + updated := rev.GetPackageRevision(ctx) return updated, false, nil } else { @@ -278,7 +278,7 @@ func (r *packageCommon) updatePackageRevision(ctx context.Context, name string, return nil, false, apierrors.NewInternalError(err) } - created := rev.GetPackageRevision() + created := rev.GetPackageRevision(ctx) return created, true, nil } } diff --git a/porch/pkg/registry/porch/packagerevision.go b/porch/pkg/registry/porch/packagerevision.go index e41eb3889b..c1c7326fba 100644 --- a/porch/pkg/registry/porch/packagerevision.go +++ b/porch/pkg/registry/porch/packagerevision.go @@ -77,7 +77,7 @@ func (r *packageRevisions) List(ctx context.Context, options *metainternalversio } if err := r.packageCommon.listPackageRevisions(ctx, filter, func(p repository.PackageRevision) error { - item := p.GetPackageRevision() + item := p.GetPackageRevision(ctx) result.Items = append(result.Items, *item) return nil }); err != nil { @@ -97,7 +97,7 @@ func (r *packageRevisions) Get(ctx context.Context, name string, options *metav1 return nil, err } - obj := pkg.GetPackageRevision() + obj := pkg.GetPackageRevision(ctx) return obj, nil } @@ -143,7 +143,7 @@ func (r *packageRevisions) Create(ctx context.Context, runtimeObject runtime.Obj return nil, apierrors.NewInternalError(err) } - created := rev.GetPackageRevision() + created := rev.GetPackageRevision(ctx) return created, nil } @@ -184,7 +184,7 @@ func (r *packageRevisions) Delete(ctx context.Context, name string, deleteValida return nil, false, err } - oldObj := oldPackage.GetPackageRevision() + oldObj := oldPackage.GetPackageRevision(ctx) repositoryObj, err := r.packageCommon.validateDelete(ctx, deleteValidation, oldObj, name, ns) if err != nil { return nil, false, err diff --git a/porch/pkg/registry/porch/packagerevisions_approval.go b/porch/pkg/registry/porch/packagerevisions_approval.go index 3e885bc67d..c35c6928d5 100644 --- a/porch/pkg/registry/porch/packagerevisions_approval.go +++ b/porch/pkg/registry/porch/packagerevisions_approval.go @@ -51,7 +51,7 @@ func (a *packageRevisionsApproval) Get(ctx context.Context, name string, options if err != nil { return nil, err } - obj := pkg.GetPackageRevision() + obj := pkg.GetPackageRevision(ctx) return obj, nil } diff --git a/porch/pkg/repository/repository.go b/porch/pkg/repository/repository.go index 77902b508c..6e66589019 100644 --- a/porch/pkg/repository/repository.go +++ b/porch/pkg/repository/repository.go @@ -60,14 +60,17 @@ type PackageRevision interface { Lifecycle() v1alpha1.PackageRevisionLifecycle // GetPackageRevision returns the PackageRevision ("DRY") API representation of this package-revision - GetPackageRevision() *v1alpha1.PackageRevision + GetPackageRevision(context.Context) *v1alpha1.PackageRevision // GetResources returns the PackageRevisionResources ("WET") API representation of this package-revision // TODO: return PackageResources or filesystem abstraction? - GetResources(ctx context.Context) (*v1alpha1.PackageRevisionResources, error) + GetResources(context.Context) (*v1alpha1.PackageRevisionResources, error) // GetUpstreamLock returns the kpt lock information. - GetUpstreamLock() (kptfile.Upstream, kptfile.UpstreamLock, error) + GetUpstreamLock(context.Context) (kptfile.Upstream, kptfile.UpstreamLock, error) + + // GetKptfile returns the Kptfile for hte package + GetKptfile(context.Context) (kptfile.KptFile, error) // GetLock returns the current revision's lock information. // This will be the upstream info for downstream revisions. diff --git a/porch/pkg/repository/util.go b/porch/pkg/repository/util.go new file mode 100644 index 0000000000..6c31f8e2e9 --- /dev/null +++ b/porch/pkg/repository/util.go @@ -0,0 +1,62 @@ +// Copyright 2022 Google LLC +// +// 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 repository + +import ( + "fmt" + + kptfile "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1" + api "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1" +) + +func ToApiReadinessGates(kf kptfile.KptFile) []api.ReadinessGate { + var readinessGates []api.ReadinessGate + if kf.Info != nil { + for _, rg := range kf.Info.ReadinessGates { + readinessGates = append(readinessGates, api.ReadinessGate{ + ConditionType: rg.ConditionType, + }) + } + } + return readinessGates +} + +func ToApiConditions(kf kptfile.KptFile) []api.Condition { + var conditions []api.Condition + if kf.Status != nil && kf.Status.Conditions != nil { + for _, s := range kf.Status.Conditions { + conditions = append(conditions, api.Condition{ + Type: s.Type, + Status: toApiConditionStatus(s.Status), + Reason: s.Reason, + Message: s.Message, + }) + } + } + return conditions +} + +func toApiConditionStatus(s kptfile.ConditionStatus) api.ConditionStatus { + switch s { + case kptfile.ConditionTrue: + return api.ConditionTrue + case kptfile.ConditionFalse: + return api.ConditionFalse + case kptfile.ConditionUnknown: + return api.ConditionUnknown + default: + panic(fmt.Errorf("unknown condition status: %v", s)) + } +}