/*
Copyright 2023.
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 v1alpha1
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
// ReleasePromotionSpec defines the desired state of ReleasePromotion.
type ReleasePromotionSpec struct {
// Protocol defines the type of repository protocol to use (e.g. oci, s3)
// +kubebuilder:validation:Enum=oci;s3
// +kubebuilder:default=oci
Protocol string `json:"protocol,omitempty"`
// ReleasesRepository specifies the path to the releases repository
// +kubebuilder:validation:Required
// +required
ReleasesRepository Repository `json:"releasesRepository,omitempty"`
// TargetRepository specifies the path where releases should be promoted to
// +kubebuilder:validation:Required
// +required
TargetRepository Repository `json:"targetRepository,omitempty"`
}
type Repository struct {
// The URL of the repository
// +kubebuilder:validation:Required
// +required
URL string `json:"url,omitempty"`
// The secret name containing the authentication credentials
// +optional
Auth *corev1.LocalObjectReference `json:"secretRef,omitempty"`
}
type ReleaseRepository struct {
// TODO: document and add examples
// oci://my-registry.my-domain/kuberik/system
// s3://my-bucket-n41nkl1n4/kuberik/system
// +kubebuilder:validation:Pattern="^(oci|s3):\\/\\/.+$"
// +optional
Url string `json:"baseUrl,omitempty"`
// The secret name containing the authentication credentials
// +optional
SecretRef *corev1.LocalObjectReference `json:"secretRef,omitempty"`
}
// ReleasePromotionStatus defines the observed state of ReleasePromotion.
type ReleasePromotionStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
// Conditions represents the current state of the release promotion process.
// +optional
// +patchMergeKey=type
// +patchStrategy=merge
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// ReleasePromotion is the Schema for the releasepromotions API.
type ReleasePromotion struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec ReleasePromotionSpec `json:"spec,omitempty"`
Status ReleasePromotionStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// ReleasePromotionList contains a list of ReleasePromotion.
type ReleasePromotionList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []ReleasePromotion `json:"items"`
}
func init() {
SchemeBuilder.Register(&ReleasePromotion{}, &ReleasePromotionList{})
}
//go:build !ignore_autogenerated
/*
Copyright 2023.
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.
*/
// Code generated by controller-gen. DO NOT EDIT.
package v1alpha1
import (
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
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 *ReleasePromotion) DeepCopyInto(out *ReleasePromotion) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReleasePromotion.
func (in *ReleasePromotion) DeepCopy() *ReleasePromotion {
if in == nil {
return nil
}
out := new(ReleasePromotion)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *ReleasePromotion) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ReleasePromotionList) DeepCopyInto(out *ReleasePromotionList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]ReleasePromotion, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReleasePromotionList.
func (in *ReleasePromotionList) DeepCopy() *ReleasePromotionList {
if in == nil {
return nil
}
out := new(ReleasePromotionList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *ReleasePromotionList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ReleasePromotionSpec) DeepCopyInto(out *ReleasePromotionSpec) {
*out = *in
in.ReleasesRepository.DeepCopyInto(&out.ReleasesRepository)
in.TargetRepository.DeepCopyInto(&out.TargetRepository)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReleasePromotionSpec.
func (in *ReleasePromotionSpec) DeepCopy() *ReleasePromotionSpec {
if in == nil {
return nil
}
out := new(ReleasePromotionSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ReleasePromotionStatus) DeepCopyInto(out *ReleasePromotionStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]metav1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReleasePromotionStatus.
func (in *ReleasePromotionStatus) DeepCopy() *ReleasePromotionStatus {
if in == nil {
return nil
}
out := new(ReleasePromotionStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ReleaseRepository) DeepCopyInto(out *ReleaseRepository) {
*out = *in
if in.SecretRef != nil {
in, out := &in.SecretRef, &out.SecretRef
*out = new(v1.LocalObjectReference)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReleaseRepository.
func (in *ReleaseRepository) DeepCopy() *ReleaseRepository {
if in == nil {
return nil
}
out := new(ReleaseRepository)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Repository) DeepCopyInto(out *Repository) {
*out = *in
if in.Auth != nil {
in, out := &in.Auth, &out.Auth
*out = new(v1.LocalObjectReference)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Repository.
func (in *Repository) DeepCopy() *Repository {
if in == nil {
return nil
}
out := new(Repository)
in.DeepCopyInto(out)
return out
}
/*
Copyright 2023.
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 v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// HealthSpec defines the desired state of Health
type HealthSpec struct {
// Reported state of this Health.
// +kubebuilder:validation:Enum=Healthy;Pending;Degraded
State HealthState `json:"state,omitempty"`
}
type HealthState string
const (
HealthStateHealthy HealthState = "Healthy"
HealthStatePending HealthState = "Pending"
HealthStateUnhealthy HealthState = "Unhealthy"
)
// HealthStatus defines the observed state of Health
type HealthStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
}
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
// Health is the Schema for the healths API
type Health struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec HealthSpec `json:"spec,omitempty"`
Status HealthStatus `json:"status,omitempty"`
}
//+kubebuilder:object:root=true
// HealthList contains a list of Health
type HealthList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Health `json:"items"`
}
func init() {
SchemeBuilder.Register(&Health{}, &HealthList{})
}
/*
Copyright 2023.
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 v1alpha1
import (
"slices"
"time"
"github.com/google/go-containerregistry/pkg/name"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// PropagationSpec defines the desired state of Propagation
type PropagationSpec struct {
PollInterval metav1.Duration `json:"pollInterval,omitempty"`
Backend PropagationBackend `json:"backend,omitempty"`
Deployment Deployment `json:"deployment,omitempty"`
}
type PropagationBackend struct {
// TODO: document and add examples
// oci://my-registry.my-domain/kuberik/system
// s3://my-bucket-n41nkl1n4/kuberik/system
// +kubebuilder:validation:Pattern="^(oci|s3):\\/\\/.+$"
// +optional
BaseUrl string `json:"baseUrl,omitempty"`
// The secret name containing the authentication credentials
// +optional
SecretRef *corev1.LocalObjectReference `json:"secretRef,omitempty"`
}
type Deployment struct {
// Name of the deployment.
Name string `json:"name,omitempty"`
// Reads the exact version that was deployed so that accurate version can be published in the status.
// +optional
Version LocalObjectField `json:"version,omitempty"`
// [!CAUTION]
// Not implemented!
//
// Selector for Health objects which will be taken into account to determine if the deployment is healthy or not.
HealthSelector HealthSelector `json:"healthSelector,omitempty"`
}
type HealthSelector struct {
// TODO:
LabelSelector metav1.LabelSelector `json:"labelSelector,omitempty"`
// TODO:
NamespaceSelector metav1.LabelSelector `json:"namespaceSelector,omitempty"`
}
type DeployAfter struct {
// Propagation will proceed only after all listed deployments report
// a healthy version for the specified amount of time.
Deployments []string `json:"deployments,omitempty"`
// Propagtion will only be performed after all the deployments specified as dependencies report
// continous healthy states for the specifed duration.
// In case there's multiple versions satisfying the condition the newest one will be used.
// +kubebuilder:validation:Type=string
// +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$"
// +required
BakeTime metav1.Duration `json:"bakeTime,omitempty"`
}
// DeployConditions defines the conditions for when and how the deployment should be propagated.
type DeployConditions struct {
// Propagation will use these conditions to determine to which version to propagate.
DeployAfter `json:"deployAfter,omitempty"`
// Propagation will not proceed until all the listed deployments have the same version as the current deployment.
DeployWith []string `json:"deployedWith,omitempty"`
}
type LocalObjectField struct {
// Kind of the referent.
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
// +optional
Kind string `json:"kind,omitempty"`
// Name of the referent.
// More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
// +optional
Name string `json:"name,omitempty"`
// API version of the referent.
// +optional
APIVersion string `json:"apiVersion,omitempty"`
// If referring to a piece of an object instead of an entire object, this string
// should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2].
// For example, if the object reference is to a container within a pod, this would take on a value like:
// "spec.containers{name}" (where "name" refers to the name of the container that triggered
// the event) or if no container name is specified "spec.containers[2]" (container with
// index 2 in this pod). This syntax is chosen only to have some well-defined way of
// referencing a part of an object.
// +required
FieldPath string `json:"fieldPath,omitempty"`
}
// PropagationStatus defines the observed state of Propagation
type PropagationStatus struct {
DeploymentStatus DeploymentStatus `json:"deploymentStatus,omitempty"`
Conditions []metav1.Condition `json:"conditions,omitempty"`
DeploymentStatusesReports []DeploymentStatusesReport `json:"deploymentStatusesReports,omitempty"`
DeployConditions DeployConditions `json:"deployConditions,omitempty"`
}
func (s *PropagationStatus) FindDeploymentStatusReport(deployment string) *DeploymentStatusesReport {
for i, report := range s.DeploymentStatusesReports {
if report.DeploymentName == deployment {
return &s.DeploymentStatusesReports[i]
}
}
return nil
}
// History of deployment statuses of an other Propagation
type DeploymentStatusesReport struct {
DeploymentName string `json:"deploymentName,omitempty"`
Statuses []DeploymentStatus `json:"statuses,omitempty"`
}
func (r *DeploymentStatusesReport) VersionHealthyDuration(version string) time.Duration {
now := time.Now()
for i, s1 := range r.Statuses {
if s1.Version != version {
continue
}
until := now
for _, s2 := range r.Statuses[i:] {
if s2.State != HealthStateHealthy {
if s2.Version == s1.Version {
return 0
} else {
until = s2.Start.Time
}
}
}
return until.Sub(s1.Start.Time)
}
return 0
}
func (r *DeploymentStatusesReport) AppendStatus(status DeploymentStatus) {
lastStatus := r.LastStatus()
if lastStatus == nil {
r.Statuses = append(r.Statuses, status)
} else if lastStatus.Version == status.Version && lastStatus.State == HealthStatePending {
lastStatus.State = status.State
if status.State == HealthStateHealthy {
lastStatus.Start = status.Start
}
} else {
r.Statuses = append(r.Statuses, status)
}
statusCount := len(r.Statuses)
if statusCount >= 2 && r.Statuses[statusCount-2].Version == status.Version &&
r.Statuses[statusCount-2].State == status.State {
r.Statuses = r.Statuses[:statusCount-1]
}
}
func (r *DeploymentStatusesReport) LastStatus() *DeploymentStatus {
statusCount := len(r.Statuses)
if statusCount == 0 {
return nil
} else {
return &r.Statuses[statusCount-1]
}
}
type DeploymentStatus struct {
Version string `json:"version,omitempty"`
Start metav1.Time `json:"start,omitempty"`
//+kubebuilder:validation:Enum=Healthy;Pending;Degraded
State HealthState `json:"state,omitempty"`
}
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
// Propagation is the Schema for the propagations API
type Propagation struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec PropagationSpec `json:"spec,omitempty"`
Status PropagationStatus `json:"status,omitempty"`
}
func (p *Propagation) NextVersion() string {
if len(p.Status.DeployConditions.DeployAfter.Deployments) == 0 {
return name.DefaultTag
}
versions := []string{}
for _, report := range p.Status.DeploymentStatusesReports {
if report.DeploymentName == p.Status.DeployConditions.DeployAfter.Deployments[0] {
for i := len(report.Statuses) - 1; i >= 0; i-- {
version := report.Statuses[i].Version
if !slices.Contains(versions, version) {
versions = append(versions, version)
}
}
}
}
versions:
for _, v := range versions {
for _, r := range p.Status.DeploymentStatusesReports {
if !slices.Contains(p.Status.DeployConditions.DeployAfter.Deployments, r.DeploymentName) {
continue
}
if p.Status.DeployConditions.DeployAfter.BakeTime.Duration > r.VersionHealthyDuration(v) {
continue versions
}
}
return v
}
return ""
}
//+kubebuilder:object:root=true
// PropagationList contains a list of Propagation
type PropagationList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Propagation `json:"items"`
}
func init() {
SchemeBuilder.Register(&Propagation{}, &PropagationList{})
}
//go:build !ignore_autogenerated
/*
Copyright 2023.
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.
*/
// Code generated by controller-gen. DO NOT EDIT.
package v1alpha1
import (
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
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 *DeployAfter) DeepCopyInto(out *DeployAfter) {
*out = *in
if in.Deployments != nil {
in, out := &in.Deployments, &out.Deployments
*out = make([]string, len(*in))
copy(*out, *in)
}
out.BakeTime = in.BakeTime
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeployAfter.
func (in *DeployAfter) DeepCopy() *DeployAfter {
if in == nil {
return nil
}
out := new(DeployAfter)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DeployConditions) DeepCopyInto(out *DeployConditions) {
*out = *in
in.DeployAfter.DeepCopyInto(&out.DeployAfter)
if in.DeployWith != nil {
in, out := &in.DeployWith, &out.DeployWith
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeployConditions.
func (in *DeployConditions) DeepCopy() *DeployConditions {
if in == nil {
return nil
}
out := new(DeployConditions)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Deployment) DeepCopyInto(out *Deployment) {
*out = *in
out.Version = in.Version
in.HealthSelector.DeepCopyInto(&out.HealthSelector)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Deployment.
func (in *Deployment) DeepCopy() *Deployment {
if in == nil {
return nil
}
out := new(Deployment)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DeploymentStatus) DeepCopyInto(out *DeploymentStatus) {
*out = *in
in.Start.DeepCopyInto(&out.Start)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeploymentStatus.
func (in *DeploymentStatus) DeepCopy() *DeploymentStatus {
if in == nil {
return nil
}
out := new(DeploymentStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DeploymentStatusesReport) DeepCopyInto(out *DeploymentStatusesReport) {
*out = *in
if in.Statuses != nil {
in, out := &in.Statuses, &out.Statuses
*out = make([]DeploymentStatus, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeploymentStatusesReport.
func (in *DeploymentStatusesReport) DeepCopy() *DeploymentStatusesReport {
if in == nil {
return nil
}
out := new(DeploymentStatusesReport)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Health) DeepCopyInto(out *Health) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
out.Spec = in.Spec
out.Status = in.Status
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Health.
func (in *Health) DeepCopy() *Health {
if in == nil {
return nil
}
out := new(Health)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Health) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HealthList) DeepCopyInto(out *HealthList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Health, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HealthList.
func (in *HealthList) DeepCopy() *HealthList {
if in == nil {
return nil
}
out := new(HealthList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *HealthList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HealthSelector) DeepCopyInto(out *HealthSelector) {
*out = *in
in.LabelSelector.DeepCopyInto(&out.LabelSelector)
in.NamespaceSelector.DeepCopyInto(&out.NamespaceSelector)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HealthSelector.
func (in *HealthSelector) DeepCopy() *HealthSelector {
if in == nil {
return nil
}
out := new(HealthSelector)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HealthSpec) DeepCopyInto(out *HealthSpec) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HealthSpec.
func (in *HealthSpec) DeepCopy() *HealthSpec {
if in == nil {
return nil
}
out := new(HealthSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HealthStatus) DeepCopyInto(out *HealthStatus) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HealthStatus.
func (in *HealthStatus) DeepCopy() *HealthStatus {
if in == nil {
return nil
}
out := new(HealthStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *LocalObjectField) DeepCopyInto(out *LocalObjectField) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LocalObjectField.
func (in *LocalObjectField) DeepCopy() *LocalObjectField {
if in == nil {
return nil
}
out := new(LocalObjectField)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Propagation) DeepCopyInto(out *Propagation) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Propagation.
func (in *Propagation) DeepCopy() *Propagation {
if in == nil {
return nil
}
out := new(Propagation)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Propagation) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PropagationBackend) DeepCopyInto(out *PropagationBackend) {
*out = *in
if in.SecretRef != nil {
in, out := &in.SecretRef, &out.SecretRef
*out = new(v1.LocalObjectReference)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PropagationBackend.
func (in *PropagationBackend) DeepCopy() *PropagationBackend {
if in == nil {
return nil
}
out := new(PropagationBackend)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PropagationList) DeepCopyInto(out *PropagationList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Propagation, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PropagationList.
func (in *PropagationList) DeepCopy() *PropagationList {
if in == nil {
return nil
}
out := new(PropagationList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *PropagationList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PropagationSpec) DeepCopyInto(out *PropagationSpec) {
*out = *in
out.PollInterval = in.PollInterval
in.Backend.DeepCopyInto(&out.Backend)
in.Deployment.DeepCopyInto(&out.Deployment)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PropagationSpec.
func (in *PropagationSpec) DeepCopy() *PropagationSpec {
if in == nil {
return nil
}
out := new(PropagationSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PropagationStatus) DeepCopyInto(out *PropagationStatus) {
*out = *in
in.DeploymentStatus.DeepCopyInto(&out.DeploymentStatus)
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]metav1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.DeploymentStatusesReports != nil {
in, out := &in.DeploymentStatusesReports, &out.DeploymentStatusesReports
*out = make([]DeploymentStatusesReport, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
in.DeployConditions.DeepCopyInto(&out.DeployConditions)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PropagationStatus.
func (in *PropagationStatus) DeepCopy() *PropagationStatus {
if in == nil {
return nil
}
out := new(PropagationStatus)
in.DeepCopyInto(out)
return out
}
package cmd
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"slices"
"github.com/go-git/go-git/v5"
"github.com/kuberik/propagation-controller/pkg/clients"
"github.com/kuberik/propagation-controller/pkg/repo/config"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)
// publishCmd represents the publish command
var publishCmd = &cobra.Command{
Use: "publish",
Short: "A brief description of your command",
Long: `A longer description that spans multiple lines and likely contains examples
and usage of using your command. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("publish called")
},
}
func init() {
rootCmd.AddCommand(publishCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// publishCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// publishCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
// publishConfig()
}
func yamlToJSON(yamlContent []byte) ([]byte, error) {
config := map[string]any{}
err := yaml.Unmarshal([]byte(yamlContent), &config)
if err != nil {
return nil, fmt.Errorf("failed to parse yaml: %w", err)
}
jsonContent, err := json.Marshal(config)
if err != nil {
return nil, fmt.Errorf("failed to marshal json: %w", err)
}
return jsonContent, nil
}
func publishConfig(path string) error {
content, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("failed to read file: %w", err)
}
contentJson, err := yamlToJSON(content)
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
cfg := config.PropagationConfig{}
if err := json.Unmarshal(contentJson, &cfg); err != nil {
return fmt.Errorf("failed to parse config: %w", err)
}
backendClient, err := clients.NewPropagationBackendClient(cfg.Backend)
if err != nil {
return fmt.Errorf("failed to create backend client: %w", err)
}
client := clients.NewPropagationClient(backendClient)
if err := client.PublishConfig(cfg.Config); err != nil {
return fmt.Errorf("failed to publish config: %w", err)
}
return nil
}
func findConfigs(repoPath string) ([]string, error) {
repo, err := git.PlainOpenWithOptions(repoPath, &git.PlainOpenOptions{DetectDotGit: true})
if err != nil {
return nil, fmt.Errorf("failed to open git repository: %w", err)
}
worktree, err := repo.Worktree()
if err != nil {
return nil, fmt.Errorf("failed to get git worktree: %w", err)
}
rootDir := worktree.Filesystem.Root()
// find all files in .kuberik directory
configs := []string{}
err = filepath.Walk(filepath.Join(rootDir, ".kuberik"), func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && slices.Contains([]string{".yaml", ".yml"}, filepath.Ext(path)) {
configs = append(configs, path)
}
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to find configs: %w", err)
}
return configs, nil
}
func PublishConfigs() error {
configs, err := findConfigs(".")
if err != nil {
return fmt.Errorf("failed to find configs: %w", err)
}
for _, config := range configs {
err := publishConfig(config)
if err != nil {
return fmt.Errorf("failed to publish config: %w", err)
}
}
return nil
}
package cmd
import (
"fmt"
"os"
"github.com/go-git/go-git/v5"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var cfgFile string
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "propagation-controller",
Short: "A brief description of your application",
Long: `A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
// Uncomment the following line if your bare application
// has an action associated with it:
// Run: func(cmd *cobra.Command, args []string) { },
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
cobra.OnInitialize(initConfig)
// Here you will define your flags and configuration settings.
// Cobra supports persistent flags, which, if defined here,
// will be global for your application.
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $(git rev-parse --show-toplevel)/.kuberik-propagation.yaml)")
// Cobra also supports local flags, which will only run
// when this action is called directly.
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
// initConfig reads in config file and ENV variables if set.
func initConfig() {
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
} else {
// Find home directory.
// Find the root of the git repository.
repo, err := git.PlainOpenWithOptions(".", &git.PlainOpenOptions{DetectDotGit: true})
if err != nil {
fmt.Fprintln(os.Stderr, "Failed to open git repository:", err)
os.Exit(1)
}
worktree, err := repo.Worktree()
if err != nil {
fmt.Fprintln(os.Stderr, "Failed to get git worktree:", err)
os.Exit(1)
}
rootDir := worktree.Filesystem.Root()
// Search config in home directory with name ".propagation-controller" (without extension).
// TODO: change config home to be root of git directory
viper.AddConfigPath(rootDir)
viper.SetConfigType("yaml")
viper.SetConfigName(".kuberik-propagation")
}
viper.AutomaticEnv() // read in environment variables that match
// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
}
}
package main
import "github.com/kuberik/propagation-controller/cmd/kpctl/cmd"
func main() {
cmd.Execute()
}
/*
Copyright 2023.
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 main
import (
"flag"
"os"
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
// to ensure that exec-entrypoint and run can make use of them.
_ "k8s.io/client-go/plugin/pkg/client/auth"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
v1alpha1 "github.com/kuberik/propagation-controller/api/v1alpha1"
"github.com/kuberik/propagation-controller/internal/controller"
"github.com/kuberik/propagation-controller/internal/controller/promotion"
"github.com/kuberik/propagation-controller/pkg/clients"
//+kubebuilder:scaffold:imports
)
var (
scheme = runtime.NewScheme()
setupLog = ctrl.Log.WithName("setup")
)
func init() {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(v1alpha1.AddToScheme(scheme))
//+kubebuilder:scaffold:scheme
}
func main() {
var metricsAddr string
var enableLeaderElection bool
var probeAddr string
flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
"Enable leader election for controller manager. "+
"Enabling this will ensure there is only one active controller manager.")
opts := zap.Options{
Development: true,
}
opts.BindFlags(flag.CommandLine)
flag.Parse()
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
Metrics: metricsserver.Options{BindAddress: metricsAddr},
HealthProbeBindAddress: probeAddr,
LeaderElection: enableLeaderElection,
LeaderElectionID: "4d52a72c.kuberik.io",
// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
// when the Manager ends. This requires the binary to immediately end when the
// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
// speeds up voluntary leader transitions as the new leader don't have to wait
// LeaseDuration time first.
//
// In the default scaffold provided, the program ends immediately after
// the manager stops, so would be fine to enable this option. However,
// if you are doing or is intended to do any operation such as perform cleanups
// after the manager stops then its usage might be unsafe.
// LeaderElectionReleaseOnCancel: true,
})
if err != nil {
setupLog.Error(err, "unable to start manager")
os.Exit(1)
}
if err = (&controller.PropagationReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
PropagationClientset: clients.NewPropagationClientset(mgr.GetClient()),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Propagation")
os.Exit(1)
}
if err = (&promotion.ReleasePromotionReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "ReleasePromotion")
os.Exit(1)
}
//+kubebuilder:scaffold:builder
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
setupLog.Error(err, "unable to set up health check")
os.Exit(1)
}
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
setupLog.Error(err, "unable to set up ready check")
os.Exit(1)
}
setupLog.Info("starting manager")
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
setupLog.Error(err, "problem running manager")
os.Exit(1)
}
}
package promotion
import (
"net/url"
"strings"
)
// import (
// "bytes"
// "context"
// "encoding/json"
// "fmt"
// "net/url"
// "reflect"
// "strings"
// "github.com/google/go-containerregistry/pkg/authn"
// "github.com/google/go-containerregistry/pkg/crane"
// "github.com/google/go-containerregistry/pkg/name"
// v1 "github.com/google/go-containerregistry/pkg/v1"
// "github.com/kuberik/propagation-controller/api/promotion/v1alpha1"
// corev1 "k8s.io/api/core/v1"
// k8stypes "k8s.io/apimachinery/pkg/types"
// "sigs.k8s.io/controller-runtime/pkg/client"
// )
// type Artifact interface {
// DigestString() (string, error)
// Bytes() ([]byte, error)
// }
// type ArtifactType int
// const (
// DeployStatusArtifactType ArtifactType = iota
// ManifestArtifactType
// DeployArtifactType
// PromotionConfigArtifactType
// )
// type ArtifactMetadata struct {
// Repository string
// Version string
// }
// type PromotionBackendClient interface {
// Fetch(ArtifactMetadata) (Artifact, error)
// Digest(ArtifactMetadata) (string, error)
// Publish(ArtifactMetadata, Artifact) error
// }
// var _ Artifact = &ociArtifact{}
// type ociArtifact struct {
// image v1.Image
// data []byte
// }
// // DigestString implements Artifact.
// func (a *ociArtifact) DigestString() (string, error) {
// digest, err := a.image.Digest()
// return digest.String(), err
// }
// // Bytes implements Artifact.
// func (a *ociArtifact) Bytes() ([]byte, error) {
// if a.data != nil {
// return a.data, nil
// }
// var extracted bytes.Buffer
// err := crane.Export(a.image, &extracted)
// if err != nil {
// return nil, err
// }
// a.data = extracted.Bytes()
// return a.data, nil
// }
// var _ PromotionBackendClient = &OCIPromotionBackendClient{}
// type OCIPromotionBackendClient struct {
// repository name.Repository
// auth *authn.AuthConfig
// }
// func NewOCIPromotionBackendClient(repository name.Repository) OCIPromotionBackendClient {
// return OCIPromotionBackendClient{
// repository: repository,
// }
// }
// func (c *OCIPromotionBackendClient) ociTagFromArtifactMetadata(m ArtifactMetadata) string {
// var subpath string
// version := m.Version
// switch m.Type {
// case DeployStatusArtifactType:
// subpath = "statuses"
// version = name.DefaultTag
// case ManifestArtifactType:
// subpath = "manifests"
// case DeployArtifactType:
// version = name.DefaultTag
// subpath = "deploy"
// case PromotionConfigArtifactType:
// version = name.DefaultTag
// subpath = "config"
// default:
// panic("unknown artifact type")
// }
// return c.repository.Registry.Repo(c.repository.RepositoryStr(), subpath, m.Deployment).Tag(version).Name()
// }
// // Digest implements PromotionBackendClient.
// func (c *OCIPromotionBackendClient) Digest(m ArtifactMetadata) (string, error) {
// return crane.Digest(c.ociTagFromArtifactMetadata(m), c.options()...)
// }
// // Fetch implements PromotionBackendClient.
// func (c *OCIPromotionBackendClient) Fetch(m ArtifactMetadata) (Artifact, error) {
// image, err := crane.Pull(c.ociTagFromArtifactMetadata(m), c.options()...)
// if err != nil {
// return nil, err
// }
// return &ociArtifact{image: image}, err
// }
// // Publish implements PromotionBackendClient.
// func (c *OCIPromotionBackendClient) Publish(m ArtifactMetadata, a Artifact) error {
// if ociArtifact, ok := a.(*ociArtifact); ok {
// return crane.Push(ociArtifact.image, c.ociTagFromArtifactMetadata(m), c.options()...)
// }
// return fmt.Errorf("incompatible artifact for OCI client")
// }
// func (c *OCIPromotionBackendClient) options() []crane.Option {
// options := []crane.Option{}
// if c.auth != nil {
// options = append(options, crane.WithAuth(authn.FromConfig(*c.auth)))
// }
// return options
// }
// type PromotionClientset struct {
// k8sClient client.Client
// clients map[k8stypes.NamespacedName]*PromotionClient
// }
func scheme(baseUrl string) (string, error) {
u, err := url.Parse(string(baseUrl))
if err != nil {
return "", err
}
return u.Scheme, nil
}
func trimScheme(baseUrl string) (string, error) {
scheme, err := scheme(baseUrl)
if err != nil {
return "", err
}
return strings.TrimPrefix(string(baseUrl), scheme+"://"), nil
}
// func NewPromotionBackendClient(baseUrl string) (PromotionBackendClient, error) {
// return newPromotionBackendClient(baseUrl, nil)
// }
// func newPromotionBackendClient(baseUrl string, secretData map[string][]byte) (PromotionBackendClient, error) {
// protocol, err := scheme(baseUrl)
// if err != nil {
// return nil, err
// }
// url, err := trimScheme(baseUrl)
// if err != nil {
// return nil, err
// }
// switch protocol {
// case "oci":
// repository, err := name.NewRepository(url)
// if err != nil {
// return nil, fmt.Errorf("failed to parse OCI repository: %w", err)
// }
// authConfig := &authn.AuthConfig{}
// if secretData[corev1.DockerConfigJsonKey] != nil {
// err = json.Unmarshal(secretData[corev1.DockerConfigJsonKey], authConfig)
// if err != nil {
// return nil, fmt.Errorf("failed to parse docker auth config: %w", err)
// }
// } else {
// authConfig = nil
// }
// return &OCIPromotionBackendClient{
// repository: repository,
// auth: authConfig,
// }, nil
// default:
// return nil, fmt.Errorf("%s backend not supported", protocol)
// }
// }
// func NewPromotionClientset(k8sClient client.Client) PromotionClientset {
// clientset := PromotionClientset{
// clients: make(map[k8stypes.NamespacedName]*PromotionClient),
// k8sClient: k8sClient,
// }
// return clientset
// }
// func (pc *PromotionClientset) Promotion(promotion v1alpha1.Promotion) (*PromotionClient, error) {
// secret := &corev1.Secret{}
// if promotion.Spec.Backend.SecretRef != nil && promotion.Spec.Backend.SecretRef.Name != "" {
// err := pc.k8sClient.Get(context.TODO(), k8stypes.NamespacedName{
// Name: promotion.Spec.Backend.SecretRef.Name,
// Namespace: promotion.Namespace,
// }, secret)
// if err != nil {
// return nil, err
// }
// }
// key := k8stypes.NamespacedName{Name: promotion.Name, Namespace: promotion.Namespace}
// newBackendClient, err := newPromotionBackendClient(promotion.Spec.Backend.BaseUrl, secret.Data)
// if err != nil {
// return nil, err
// }
// var client *PromotionClient
// if c, ok := pc.clients[key]; ok {
// client = c
// client.client.PromotionBackendClient = newBackendClient
// } else {
// c := NewPromotionClient(newBackendClient)
// client = &c
// }
// pc.clients[key] = client
// return client, nil
// }
// var _ PromotionBackendClient = &CachedPromotionBackendClient{}
// type CachedPromotionBackendClient struct {
// PromotionBackendClient
// fetchCache map[ArtifactMetadata]Artifact
// publishCache map[ArtifactMetadata]Artifact
// }
// func NewCachedPromotionBackendClient(client PromotionBackendClient) CachedPromotionBackendClient {
// return CachedPromotionBackendClient{
// PromotionBackendClient: client,
// fetchCache: make(map[ArtifactMetadata]Artifact),
// publishCache: make(map[ArtifactMetadata]Artifact),
// }
// }
// // Fetch implements PromotionBackendClient.
// func (c *CachedPromotionBackendClient) Fetch(metadata ArtifactMetadata) (Artifact, error) {
// if cached, ok := c.fetchCache[metadata]; ok {
// // Manifest artifact should be immutable
// if metadata.Type == ManifestArtifactType && metadata.Version != name.DefaultTag {
// return cached, nil
// }
// cachedDigest, digestErr := cached.DigestString()
// remoteDigest, err := c.Digest(metadata)
// if err == nil && digestErr == nil && remoteDigest == cachedDigest {
// return cached, nil
// }
// }
// a, err := c.PromotionBackendClient.Fetch(metadata)
// if err != nil {
// return nil, err
// }
// c.fetchCache[metadata] = a
// return a, nil
// }
// // Publish implements PromotionBackendClient.
// func (c *CachedPromotionBackendClient) Publish(metadata ArtifactMetadata, a Artifact) error {
// if cached, ok := c.publishCache[metadata]; ok {
// cachedBytes, err := cached.Bytes()
// if err != nil {
// return err
// }
// publishBytes, err := a.Bytes()
// if err != nil {
// return err
// }
// if reflect.DeepEqual(cachedBytes, publishBytes) {
// return nil
// }
// }
// err := c.PromotionBackendClient.Publish(metadata, a)
// if err != nil {
// return err
// }
// c.publishCache[metadata] = a
// return nil
// }
// type PromotionClient struct {
// client CachedPromotionBackendClient
// }
// func NewPromotionClient(client PromotionBackendClient) PromotionClient {
// return PromotionClient{
// client: NewCachedPromotionBackendClient(client),
// }
// }
// func (c *PromotionClient) publishArtifact(metadata ArtifactMetadata, data interface{}) error {
// artifact, err := c.client.NewArtifact(data)
// if err != nil {
// return err
// }
// return c.client.Publish(metadata, artifact)
// }
// func (c *PromotionClient) fetchArtifact(metadata ArtifactMetadata, dest interface{}) error {
// artifact, err := c.client.Fetch(metadata)
// if err != nil {
// return err
// }
// return c.client.ParseArtifact(artifact, dest)
// }
// func (c *PromotionClient) Promote(deployment, version string) error {
// artifact, err := c.client.Fetch(
// ArtifactMetadata{Type: ManifestArtifactType, Deployment: deployment, Version: version},
// )
// if err != nil {
// return err
// }
// if err := c.client.Publish(
// ArtifactMetadata{Type: DeployArtifactType, Deployment: deployment},
// artifact,
// ); err != nil {
// return err
// }
// return nil
// }
/*
Copyright 2023.
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 promotion
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"github.com/google/go-containerregistry/pkg/crane"
"github.com/kuberik/propagation-controller/api/promotion/v1alpha1"
promotionv1alpha1 "github.com/kuberik/propagation-controller/api/promotion/v1alpha1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// ReleasePromotionReconciler reconciles a ReleasePromotion object
type ReleasePromotionReconciler struct {
client.Client
Scheme *runtime.Scheme
}
// +kubebuilder:rbac:groups=promotion.kuberik.io,resources=releasepromotions,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=promotion.kuberik.io,resources=releasepromotions/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=promotion.kuberik.io,resources=releasepromotions/finalizers,verbs=update
// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the ReleasePromotion object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.4/pkg/reconcile
func (r *ReleasePromotionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
_ = logf.FromContext(ctx)
releasePromotion := v1alpha1.ReleasePromotion{}
if err := r.Client.Get(ctx, req.NamespacedName, &releasePromotion); err != nil {
if client.IgnoreNotFound(err) != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
releases, err := crane.ListTags(releasePromotion.Spec.ReleasesRepository.URL)
if err != nil {
logf.FromContext(ctx).Error(err, "Failed to list tags from releases repository")
changed := meta.SetStatusCondition(&releasePromotion.Status.Conditions, metav1.Condition{
Type: "Available",
Status: metav1.ConditionFalse,
LastTransitionTime: metav1.Now(),
Reason: "ListTagsFailed",
Message: err.Error(),
})
if changed {
r.Status().Update(ctx, &releasePromotion)
}
return ctrl.Result{}, err
}
if releasePromotion.Spec.Protocol == "oci" {
err = crane.Copy(
fmt.Sprintf("%s:%s", releasePromotion.Spec.ReleasesRepository.URL, releases[0]),
fmt.Sprintf("%s:latest", releasePromotion.Spec.TargetRepository.URL),
)
if err != nil {
logf.FromContext(ctx).Error(err, "Failed to promote artifact from releases repository to target repository")
changed := meta.SetStatusCondition(&releasePromotion.Status.Conditions, metav1.Condition{
Type: "Available",
Status: metav1.ConditionFalse,
LastTransitionTime: metav1.Now(),
Reason: "ArtifactPromotionFailed",
Message: err.Error(),
})
if changed {
r.Status().Update(ctx, &releasePromotion)
}
return ctrl.Result{}, err
}
} else {
// TODO(user): implement s3 protocol
}
return ctrl.Result{}, nil
}
// SetupWithManager sets up the controller with the Manager.
func (r *ReleasePromotionReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&promotionv1alpha1.ReleasePromotion{}).
Named("promotion-releasepromotion").
Complete(r)
}
/*
Copyright 2023.
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 controller
import (
"context"
"fmt"
"slices"
"time"
v1alpha1 "github.com/kuberik/propagation-controller/api/v1alpha1"
"github.com/kuberik/propagation-controller/pkg/clients"
"github.com/kuberik/propagation-controller/pkg/repo/config"
"golang.org/x/sync/errgroup"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
kyaml_utils "sigs.k8s.io/kustomize/kyaml/utils"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
type PropagationReadyReason string
const (
BackendInitFailedPropagationReadyReason PropagationReadyReason = "BackendInitFailed"
ConfigInitFailedPropagationReadyReason PropagationReadyReason = "ConfigInitFailed"
VersionMissingPropagationReadyReason PropagationReadyReason = "VersionMissing"
ReadyPropagationReadyReason PropagationReadyReason = "Ready"
)
// PropagationReconciler reconciles a Propagation object
type PropagationReconciler struct {
client.Client
Scheme *runtime.Scheme
clients.PropagationClientset
}
//+kubebuilder:rbac:groups=kuberik.io,resources=propagations,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=kuberik.io,resources=propagations/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=kuberik.io,resources=propagations/finalizers,verbs=update
// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
func (r *PropagationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := log.FromContext(ctx).WithName(req.NamespacedName.String())
propagation := &v1alpha1.Propagation{}
err := r.Client.Get(ctx, req.NamespacedName, propagation)
if err != nil {
return ctrl.Result{}, err
}
propagationClient, err := r.Propagation(*propagation)
if err != nil {
return r.SetReadyConditionFalse(ctx, propagation, err, BackendInitFailedPropagationReadyReason)
}
config, err := propagationClient.GetConfig()
if err != nil {
return r.SetReadyConditionFalse(ctx, propagation, err, ConfigInitFailedPropagationReadyReason)
}
deployConditions, err := deployConditionsFromConfig(*propagation, *config)
if err != nil {
return r.SetReadyConditionFalse(ctx, propagation, err, ConfigInitFailedPropagationReadyReason)
}
propagation.Status.DeployConditions = *deployConditions
if err := r.Client.Status().Update(ctx, propagation); err != nil {
return ctrl.Result{}, err
}
version, err := r.getVersion(ctx, *propagation)
if err != nil {
return r.SetReadyConditionFalse(ctx, propagation, err, VersionMissingPropagationReadyReason)
}
if readyCondition := meta.FindStatusCondition(
propagation.Status.Conditions, v1alpha1.ReadyCondition,
); readyCondition == nil || readyCondition.Status != metav1.ConditionTrue {
meta.SetStatusCondition(&propagation.Status.Conditions, metav1.Condition{
Type: v1alpha1.ReadyCondition,
Status: metav1.ConditionTrue,
Message: "Propagation ready",
ObservedGeneration: propagation.Generation,
Reason: string(ReadyPropagationReadyReason),
})
if err := r.Client.Status().Update(ctx, propagation); err != nil {
return ctrl.Result{}, err
}
}
// TODO: setting to healthy always for starters because we don't have any health controllers and will at least make controller somewhat useful
deploymentState := v1alpha1.HealthStateHealthy
if propagation.Status.DeploymentStatus.Version != version ||
propagation.Status.DeploymentStatus.State != deploymentState {
propagation.Status.DeploymentStatus = v1alpha1.DeploymentStatus{
Version: version,
State: v1alpha1.HealthStateHealthy,
}
if err := propagationClient.PublishStatus(
propagation.Spec.Deployment.Name,
propagation.Status.DeploymentStatus,
); err != nil {
return ctrl.Result{}, err
}
err = r.Client.Status().Update(ctx, propagation)
if err != nil {
return ctrl.Result{}, nil
}
}
// if status healthy, propagate
// 1. fetch statuses
var getStatusErrors errgroup.Group
requiredStatuses := append(propagation.Status.DeployConditions.DeployWith, propagation.Status.DeployConditions.DeployAfter.Deployments...)
reports := make([]v1alpha1.DeploymentStatusesReport, len(requiredStatuses))
for i, deployment := range requiredStatuses {
report := propagation.Status.FindDeploymentStatusReport(deployment)
if report == nil {
report = &v1alpha1.DeploymentStatusesReport{
DeploymentName: deployment,
}
}
reports[i] = *report
i, deployment := i, deployment
getStatusErrors.Go(func() error {
status, err := propagationClient.GetStatus(deployment)
if err != nil {
return err
}
status.Start = metav1.Now()
reports[i].AppendStatus(*status)
return nil
})
}
statusesErr := getStatusErrors.Wait()
if statusesErr != nil {
meta.SetStatusCondition(&propagation.Status.Conditions, metav1.Condition{
Type: "StatusesFetched",
Status: metav1.ConditionFalse,
Message: fmt.Sprintf("Error fetching statuses: %v", statusesErr),
ObservedGeneration: propagation.Generation,
Reason: "StatusesFetchFailed",
})
r.Client.Status().Update(ctx, propagation)
return ctrl.Result{}, statusesErr
}
meta.SetStatusCondition(&propagation.Status.Conditions, metav1.Condition{
Type: "StatusesFetched",
Status: metav1.ConditionTrue,
Message: "Statuses fetched",
ObservedGeneration: propagation.Generation,
Reason: "StatusesFetchSuccess",
})
propagation.Status.DeploymentStatusesReports = reports
if err := r.Client.Status().Update(ctx, propagation); err != nil {
return ctrl.Result{}, err
}
waitTime := propagation.Spec.PollInterval.Duration
if waitTime == 0 {
waitTime = time.Minute
}
// check if all deployWith are on the same version as current deployment
for _, deployment := range propagation.Status.DeployConditions.DeployWith {
deploymentStatus := propagation.Status.FindDeploymentStatusReport(deployment)
if deploymentStatus == nil ||
deploymentStatus.LastStatus().Version != version ||
deploymentStatus.LastStatus().State != v1alpha1.HealthStateHealthy ||
deploymentStatus.VersionHealthyDuration(version) < config.DeploymentBakeTime(deployment) {
log.Info(fmt.Sprintf("Waiting for deployment %s to bake", deployment))
return ctrl.Result{
Requeue: true,
RequeueAfter: waitTime,
}, nil
}
}
propagateVersion := propagation.NextVersion()
if propagateVersion == "" {
// TODO: Calculate from status how long to wait
log.Info("No candidate version to propagate")
return ctrl.Result{
Requeue: true,
RequeueAfter: waitTime,
}, nil
}
log.Info(fmt.Sprintf("Propagating to version %s", propagateVersion))
if err := propagationClient.Propagate(propagation.Spec.Deployment.Name, propagateVersion); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{
Requeue: true,
RequeueAfter: waitTime,
}, nil
}
func (r *PropagationReconciler) getVersion(ctx context.Context, propagation v1alpha1.Propagation) (string, error) {
versionObject := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": propagation.Spec.Deployment.Version.APIVersion,
"kind": propagation.Spec.Deployment.Version.Kind,
},
}
if propagation.Spec.Deployment.Version.Name == "" {
return "", fmt.Errorf("missing version's object name")
}
if propagation.Spec.Deployment.Version.FieldPath == "" {
return "", fmt.Errorf("missing version's object fieldPath")
}
err := r.Client.Get(ctx, types.NamespacedName{Name: propagation.Spec.Deployment.Version.Name, Namespace: propagation.Namespace}, versionObject)
if err != nil {
return "", err
}
objRNode, err := yaml.FromMap(versionObject.Object)
if err != nil {
return "", err
}
fieldPath := kyaml_utils.SmarterPathSplitter(propagation.Spec.Deployment.Version.FieldPath, ".")
rn, err := objRNode.Pipe(yaml.Lookup(fieldPath...))
if err != nil {
return "", fmt.Errorf("error looking up version path: %w", err)
}
if rn.IsNilOrEmpty() {
return "", fmt.Errorf("version is empty")
}
if !rn.IsStringValue() {
return "", fmt.Errorf("version field is not a string")
}
return rn.Document().Value, nil
}
func (r *PropagationReconciler) SetReadyConditionFalse(ctx context.Context, propagation *v1alpha1.Propagation, err error, reason PropagationReadyReason) (ctrl.Result, error) {
meta.SetStatusCondition(&propagation.Status.Conditions, metav1.Condition{
Type: v1alpha1.ReadyCondition,
Message: err.Error(),
Status: metav1.ConditionFalse,
ObservedGeneration: propagation.Generation,
Reason: string(reason),
})
r.Client.Status().Update(ctx, propagation)
return ctrl.Result{}, err
}
// deployAfterFromConfig determines based on the global deployment config which deployments precede the current one in the propagation sequence.
// It identifies the the wave and environment of the current deployment and returns the deployments from the previous wave.
func deployAfterFromConfig(propagation v1alpha1.Propagation, c config.Config) (*v1alpha1.DeployAfter, error) {
var lastWave config.Wave
for _, env := range c.Environments {
for _, wave := range env.Waves {
for _, deployment := range wave.Deployments {
if deployment == propagation.Spec.Deployment.Name {
return &v1alpha1.DeployAfter{
Deployments: lastWave.Deployments,
BakeTime: lastWave.BakeTime,
}, nil
}
}
lastWave = wave
}
}
return nil, fmt.Errorf("failed to find the config for '%s' deployment", propagation.Spec.Deployment.Name)
}
// deployWithFromConfig deploys determines which deployments needs to have same version as the current deployment before proceeding to propagate to next version.
// The returned list of deployments are all deployments from all subsequent waves in the same environment.
func deployWithFromConfig(propagation v1alpha1.Propagation, c config.Config) ([]string, error) {
deployments := []string{}
for _, env := range c.Environments {
for waveIdx, wave := range env.Waves {
if slices.Contains(wave.Deployments, propagation.Spec.Deployment.Name) {
if waveIdx == 0 {
for _, wave := range env.Waves[waveIdx+1:] {
deployments = append(deployments, wave.Deployments...)
}
return deployments, nil
}
return []string{}, nil
}
}
}
return nil, fmt.Errorf("failed to find the config for '%s' deployment", propagation.Spec.Deployment.Name)
}
func deployConditionsFromConfig(propagation v1alpha1.Propagation, c config.Config) (*v1alpha1.DeployConditions, error) {
deployAfter, err := deployAfterFromConfig(propagation, c)
if err != nil {
return nil, err
}
deployWith, err := deployWithFromConfig(propagation, c)
if err != nil {
return nil, err
}
return &v1alpha1.DeployConditions{
DeployAfter: *deployAfter,
DeployWith: deployWith,
}, nil
}
// SetupWithManager sets up the controller with the Manager.
func (r *PropagationReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&v1alpha1.Propagation{}).
Complete(r)
}
package clients
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/url"
"reflect"
"strings"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/crane"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/mutate"
"github.com/google/go-containerregistry/pkg/v1/static"
"github.com/google/go-containerregistry/pkg/v1/types"
"github.com/kuberik/propagation-controller/api/v1alpha1"
"github.com/kuberik/propagation-controller/pkg/repo/config"
corev1 "k8s.io/api/core/v1"
k8stypes "k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)
type Artifact interface {
DigestString() (string, error)
Bytes() ([]byte, error)
}
type ArtifactType int
const (
DeployStatusArtifactType ArtifactType = iota
ManifestArtifactType
DeployArtifactType
PropagationConfigArtifactType
)
type ArtifactMetadata struct {
Deployment string
Type ArtifactType
Version string
}
type PropagationBackendClient interface {
Fetch(ArtifactMetadata) (Artifact, error)
Digest(ArtifactMetadata) (string, error)
Publish(ArtifactMetadata, Artifact) error
NewArtifact(data any) (Artifact, error)
ParseArtifact(a Artifact, dest any) error
}
var _ Artifact = &ociArtifact{}
type ociArtifact struct {
image v1.Image
data []byte
}
// DigestString implements Artifact.
func (a *ociArtifact) DigestString() (string, error) {
digest, err := a.image.Digest()
return digest.String(), err
}
// Bytes implements Artifact.
func (a *ociArtifact) Bytes() ([]byte, error) {
if a.data != nil {
return a.data, nil
}
var extracted bytes.Buffer
err := crane.Export(a.image, &extracted)
if err != nil {
return nil, err
}
a.data = extracted.Bytes()
return a.data, nil
}
var _ PropagationBackendClient = &OCIPropagationBackendClient{}
type OCIPropagationBackendClient struct {
repository name.Repository
auth *authn.AuthConfig
}
func NewOCIPropagationBackendClient(repository name.Repository) OCIPropagationBackendClient {
return OCIPropagationBackendClient{
repository: repository,
}
}
func (c *OCIPropagationBackendClient) ociTagFromArtifactMetadata(m ArtifactMetadata) string {
var subpath string
version := m.Version
switch m.Type {
case DeployStatusArtifactType:
subpath = "statuses"
version = name.DefaultTag
case ManifestArtifactType:
subpath = "manifests"
case DeployArtifactType:
version = name.DefaultTag
subpath = "deploy"
case PropagationConfigArtifactType:
version = name.DefaultTag
subpath = "config"
default:
panic("unknown artifact type")
}
return c.repository.Registry.Repo(c.repository.RepositoryStr(), subpath, m.Deployment).Tag(version).Name()
}
// Digest implements PropagationBackendClient.
func (c *OCIPropagationBackendClient) Digest(m ArtifactMetadata) (string, error) {
return crane.Digest(c.ociTagFromArtifactMetadata(m), c.options()...)
}
// Fetch implements PropagationBackendClient.
func (c *OCIPropagationBackendClient) Fetch(m ArtifactMetadata) (Artifact, error) {
image, err := crane.Pull(c.ociTagFromArtifactMetadata(m), c.options()...)
if err != nil {
return nil, err
}
return &ociArtifact{image: image}, err
}
// Publish implements PropagationBackendClient.
func (c *OCIPropagationBackendClient) Publish(m ArtifactMetadata, a Artifact) error {
if ociArtifact, ok := a.(*ociArtifact); ok {
return crane.Push(ociArtifact.image, c.ociTagFromArtifactMetadata(m), c.options()...)
}
return fmt.Errorf("incompatible artifact for OCI client")
}
// NewArtifact implements PropagationBackendClient.
func (*OCIPropagationBackendClient) NewArtifact(data any) (Artifact, error) {
artifactJSON, err := json.Marshal(data)
if err != nil {
return nil, err
}
layer := static.NewLayer(artifactJSON, types.MediaType("application/json"))
image, err := mutate.AppendLayers(empty.Image, layer)
if err != nil {
return nil, err
}
return &ociArtifact{image: image}, nil
}
// ParseArtifact implements PropagationBackendClient.
func (c *OCIPropagationBackendClient) ParseArtifact(a Artifact, dest any) error {
artifactData, err := a.Bytes()
if err != nil {
return err
}
return json.Unmarshal(artifactData, dest)
}
func (c *OCIPropagationBackendClient) options() []crane.Option {
options := []crane.Option{}
if c.auth != nil {
options = append(options, crane.WithAuth(authn.FromConfig(*c.auth)))
}
return options
}
type PropagationClientset struct {
k8sClient client.Client
clients map[k8stypes.NamespacedName]*PropagationClient
}
func scheme(baseUrl string) (string, error) {
u, err := url.Parse(string(baseUrl))
if err != nil {
return "", err
}
return u.Scheme, nil
}
func trimScheme(baseUrl string) (string, error) {
scheme, err := scheme(baseUrl)
if err != nil {
return "", err
}
return strings.TrimPrefix(string(baseUrl), scheme+"://"), nil
}
func NewPropagationBackendClient(baseUrl string) (PropagationBackendClient, error) {
return newPropagationBackendClient(baseUrl, nil)
}
func newPropagationBackendClient(baseUrl string, secretData map[string][]byte) (PropagationBackendClient, error) {
protocol, err := scheme(baseUrl)
if err != nil {
return nil, err
}
url, err := trimScheme(baseUrl)
if err != nil {
return nil, err
}
switch protocol {
case "oci":
repository, err := name.NewRepository(url)
if err != nil {
return nil, fmt.Errorf("failed to parse OCI repository: %w", err)
}
authConfig := &authn.AuthConfig{}
if secretData[corev1.DockerConfigJsonKey] != nil {
err = json.Unmarshal(secretData[corev1.DockerConfigJsonKey], authConfig)
if err != nil {
return nil, fmt.Errorf("failed to parse docker auth config: %w", err)
}
} else {
authConfig = nil
}
return &OCIPropagationBackendClient{
repository: repository,
auth: authConfig,
}, nil
default:
return nil, fmt.Errorf("%s backend not supported", protocol)
}
}
func NewPropagationClientset(k8sClient client.Client) PropagationClientset {
clientset := PropagationClientset{
clients: make(map[k8stypes.NamespacedName]*PropagationClient),
k8sClient: k8sClient,
}
return clientset
}
func (pc *PropagationClientset) Propagation(propagation v1alpha1.Propagation) (*PropagationClient, error) {
secret := &corev1.Secret{}
if propagation.Spec.Backend.SecretRef != nil && propagation.Spec.Backend.SecretRef.Name != "" {
err := pc.k8sClient.Get(context.TODO(), k8stypes.NamespacedName{
Name: propagation.Spec.Backend.SecretRef.Name,
Namespace: propagation.Namespace,
}, secret)
if err != nil {
return nil, err
}
}
key := k8stypes.NamespacedName{Name: propagation.Name, Namespace: propagation.Namespace}
newBackendClient, err := newPropagationBackendClient(propagation.Spec.Backend.BaseUrl, secret.Data)
if err != nil {
return nil, err
}
var client *PropagationClient
if c, ok := pc.clients[key]; ok {
client = c
client.client.PropagationBackendClient = newBackendClient
} else {
c := NewPropagationClient(newBackendClient)
client = &c
}
pc.clients[key] = client
return client, nil
}
var _ PropagationBackendClient = &CachedPropagationBackendClient{}
type CachedPropagationBackendClient struct {
PropagationBackendClient
fetchCache map[ArtifactMetadata]Artifact
publishCache map[ArtifactMetadata]Artifact
}
func NewCachedPropagationBackendClient(client PropagationBackendClient) CachedPropagationBackendClient {
return CachedPropagationBackendClient{
PropagationBackendClient: client,
fetchCache: make(map[ArtifactMetadata]Artifact),
publishCache: make(map[ArtifactMetadata]Artifact),
}
}
// Fetch implements PropagationBackendClient.
func (c *CachedPropagationBackendClient) Fetch(metadata ArtifactMetadata) (Artifact, error) {
if cached, ok := c.fetchCache[metadata]; ok {
// Manifest artifact should be immutable
if metadata.Type == ManifestArtifactType && metadata.Version != name.DefaultTag {
return cached, nil
}
cachedDigest, digestErr := cached.DigestString()
remoteDigest, err := c.Digest(metadata)
if err == nil && digestErr == nil && remoteDigest == cachedDigest {
return cached, nil
}
}
a, err := c.PropagationBackendClient.Fetch(metadata)
if err != nil {
return nil, err
}
c.fetchCache[metadata] = a
return a, nil
}
// Publish implements PropagationBackendClient.
func (c *CachedPropagationBackendClient) Publish(metadata ArtifactMetadata, a Artifact) error {
if cached, ok := c.publishCache[metadata]; ok {
cachedBytes, err := cached.Bytes()
if err != nil {
return err
}
publishBytes, err := a.Bytes()
if err != nil {
return err
}
if reflect.DeepEqual(cachedBytes, publishBytes) {
return nil
}
}
err := c.PropagationBackendClient.Publish(metadata, a)
if err != nil {
return err
}
c.publishCache[metadata] = a
return nil
}
type PropagationClient struct {
client CachedPropagationBackendClient
}
func NewPropagationClient(client PropagationBackendClient) PropagationClient {
return PropagationClient{
client: NewCachedPropagationBackendClient(client),
}
}
func (c *PropagationClient) publishArtifact(metadata ArtifactMetadata, data interface{}) error {
artifact, err := c.client.NewArtifact(data)
if err != nil {
return err
}
return c.client.Publish(metadata, artifact)
}
func (c *PropagationClient) PublishStatus(deployment string, status v1alpha1.DeploymentStatus) error {
return c.publishArtifact(ArtifactMetadata{Deployment: deployment, Type: DeployStatusArtifactType}, status)
}
func (c *PropagationClient) fetchArtifact(metadata ArtifactMetadata, dest interface{}) error {
artifact, err := c.client.Fetch(metadata)
if err != nil {
return err
}
return c.client.ParseArtifact(artifact, dest)
}
func (c *PropagationClient) GetStatus(deployment string) (*v1alpha1.DeploymentStatus, error) {
status := &v1alpha1.DeploymentStatus{}
err := c.fetchArtifact(ArtifactMetadata{Type: DeployStatusArtifactType, Deployment: deployment}, status)
if err != nil {
return nil, err
}
return status, nil
}
func (c *PropagationClient) Propagate(deployment, version string) error {
artifact, err := c.client.Fetch(
ArtifactMetadata{Type: ManifestArtifactType, Deployment: deployment, Version: version},
)
if err != nil {
return err
}
if err := c.client.Publish(
ArtifactMetadata{Type: DeployArtifactType, Deployment: deployment},
artifact,
); err != nil {
return err
}
return nil
}
func (c *PropagationClient) PublishConfig(config config.Config) error {
return c.publishArtifact(
ArtifactMetadata{Type: PropagationConfigArtifactType},
config,
)
}
func (c *PropagationClient) GetConfig() (*config.Config, error) {
config := &config.Config{}
err := c.fetchArtifact(ArtifactMetadata{Type: PropagationConfigArtifactType}, config)
if err != nil {
return nil, err
}
return config, nil
}
package config
import (
"time"
"slices"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type PropagationConfig struct {
Backend string `json:"backend,omitempty"`
Config `json:",inline"`
}
type Config struct {
Environments []Environment `json:"environments,omitempty"`
}
func (c *Config) DeploymentBakeTime(deployment string) time.Duration {
for _, env := range c.Environments {
for _, wave := range env.Waves {
if slices.Contains(wave.Deployments, deployment) {
return wave.BakeTime.Duration
}
}
}
return 0
}
type Environment struct {
Name string `json:"name,omitempty"`
Waves []Wave `json:"waves,omitempty"`
ReleaseCadence ReleaseCadence `json:"releaseCadence,omitempty"`
}
type Wave struct {
BakeTime metav1.Duration `json:"bakeTime,omitempty"`
// TODO: this should be populated from manifests
Deployments []string `json:"deployments,omitempty"`
}
type ReleaseCadence struct {
Schedule string `json:"schedule,omitempty"`
WaitTime metav1.Duration `json:"waitTime,omitempty"`
}
package testhelpers
import (
"net/http"
"net/http/httptest"
"github.com/google/go-containerregistry/pkg/registry"
)
func LocalRegistry(intercepts ...http.Handler) *httptest.Server {
registry := registry.New()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
for _, i := range intercepts {
i.ServeHTTP(w, r)
}
registry.ServeHTTP(w, r)
}))
}