Merge pull request #788 from fluxcd/oci
[RFC-0003] Implement OCIRepository reconciliation
This commit is contained in:
commit
1db1626fe1
14
Makefile
14
Makefile
|
@ -9,6 +9,9 @@ LIBGIT2_TAG ?= v0.2.0
|
|||
# Allows for defining additional Go test args, e.g. '-tags integration'.
|
||||
GO_TEST_ARGS ?= -race
|
||||
|
||||
# Allows for filtering tests based on the specified prefix
|
||||
GO_TEST_PREFIX ?=
|
||||
|
||||
# Allows for defining additional Docker buildx arguments,
|
||||
# e.g. '--push'.
|
||||
BUILD_ARGS ?=
|
||||
|
@ -69,7 +72,7 @@ build: check-deps $(LIBGIT2) ## Build manager binary
|
|||
go build $(GO_STATIC_FLAGS) -o $(BUILD_DIR)/bin/manager main.go
|
||||
|
||||
KUBEBUILDER_ASSETS?="$(shell $(ENVTEST) --arch=$(ENVTEST_ARCH) use -i $(ENVTEST_KUBERNETES_VERSION) --bin-dir=$(ENVTEST_ASSETS_DIR) -p path)"
|
||||
test: $(LIBGIT2) install-envtest test-api check-deps ## Run tests
|
||||
test: $(LIBGIT2) install-envtest test-api check-deps ## Run all tests
|
||||
HTTPS_PROXY="" HTTP_PROXY="" \
|
||||
KUBEBUILDER_ASSETS=$(KUBEBUILDER_ASSETS) \
|
||||
GIT_CONFIG_GLOBAL=/dev/null \
|
||||
|
@ -78,6 +81,15 @@ test: $(LIBGIT2) install-envtest test-api check-deps ## Run tests
|
|||
$(GO_TEST_ARGS) \
|
||||
-coverprofile cover.out
|
||||
|
||||
test-ctrl: $(LIBGIT2) install-envtest test-api check-deps ## Run controller tests
|
||||
HTTPS_PROXY="" HTTP_PROXY="" \
|
||||
KUBEBUILDER_ASSETS=$(KUBEBUILDER_ASSETS) \
|
||||
GIT_CONFIG_GLOBAL=/dev/null \
|
||||
go test $(GO_STATIC_FLAGS) \
|
||||
-run "^$(GO_TEST_PREFIX).*" \
|
||||
-v ./controllers \
|
||||
-coverprofile cover.out
|
||||
|
||||
check-deps:
|
||||
ifeq ($(shell uname -s),Darwin)
|
||||
if ! command -v pkg-config &> /dev/null; then echo "pkg-config is required"; exit 1; fi
|
||||
|
|
3
PROJECT
3
PROJECT
|
@ -25,4 +25,7 @@ resources:
|
|||
- group: source
|
||||
kind: Bucket
|
||||
version: v1beta1
|
||||
- group: source
|
||||
kind: OCIRepository
|
||||
version: v1beta2
|
||||
version: "2"
|
||||
|
|
|
@ -54,6 +54,10 @@ type Artifact struct {
|
|||
// Size is the number of bytes in the file.
|
||||
// +optional
|
||||
Size *int64 `json:"size,omitempty"`
|
||||
|
||||
// Metadata holds upstream information such as OCI annotations.
|
||||
// +optional
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// HasRevision returns if the given revision matches the current Revision of
|
||||
|
|
|
@ -0,0 +1,226 @@
|
|||
/*
|
||||
Copyright 2022 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package v1beta2
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/fluxcd/pkg/apis/meta"
|
||||
)
|
||||
|
||||
const (
|
||||
// OCIRepositoryKind is the string representation of a OCIRepository.
|
||||
OCIRepositoryKind = "OCIRepository"
|
||||
|
||||
// OCIRepositoryPrefix is the prefix used for OCIRepository URLs.
|
||||
OCIRepositoryPrefix = "oci://"
|
||||
|
||||
// GenericOCIProvider provides support for authentication using static credentials
|
||||
// for any OCI compatible API such as Docker Registry, GitHub Container Registry,
|
||||
// Docker Hub, Quay, etc.
|
||||
GenericOCIProvider string = "generic"
|
||||
|
||||
// AmazonOCIProvider provides support for OCI authentication using AWS IRSA.
|
||||
AmazonOCIProvider string = "aws"
|
||||
|
||||
// GoogleOCIProvider provides support for OCI authentication using GCP workload identity.
|
||||
GoogleOCIProvider string = "gcp"
|
||||
|
||||
// AzureOCIProvider provides support for OCI authentication using a Azure Service Principal,
|
||||
// Managed Identity or Shared Key.
|
||||
AzureOCIProvider string = "azure"
|
||||
)
|
||||
|
||||
// OCIRepositorySpec defines the desired state of OCIRepository
|
||||
type OCIRepositorySpec struct {
|
||||
// URL is a reference to an OCI artifact repository hosted
|
||||
// on a remote container registry.
|
||||
// +kubebuilder:validation:Pattern="^oci://.*$"
|
||||
// +required
|
||||
URL string `json:"url"`
|
||||
|
||||
// The OCI reference to pull and monitor for changes,
|
||||
// defaults to the latest tag.
|
||||
// +optional
|
||||
Reference *OCIRepositoryRef `json:"ref,omitempty"`
|
||||
|
||||
// The provider used for authentication, can be 'aws', 'azure', 'gcp' or 'generic'.
|
||||
// When not specified, defaults to 'generic'.
|
||||
// +kubebuilder:validation:Enum=generic;aws;azure;gcp
|
||||
// +kubebuilder:default:=generic
|
||||
// +optional
|
||||
Provider string `json:"provider,omitempty"`
|
||||
|
||||
// SecretRef contains the secret name containing the registry login
|
||||
// credentials to resolve image metadata.
|
||||
// The secret must be of type kubernetes.io/dockerconfigjson.
|
||||
// +optional
|
||||
SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"`
|
||||
|
||||
// ServiceAccountName is the name of the Kubernetes ServiceAccount used to authenticate
|
||||
// the image pull if the service account has attached pull secrets. For more information:
|
||||
// https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#add-imagepullsecrets-to-a-service-account
|
||||
// +optional
|
||||
ServiceAccountName string `json:"serviceAccountName,omitempty"`
|
||||
|
||||
// CertSecretRef can be given the name of a secret containing
|
||||
// either or both of
|
||||
//
|
||||
// - a PEM-encoded client certificate (`certFile`) and private
|
||||
// key (`keyFile`);
|
||||
// - a PEM-encoded CA certificate (`caFile`)
|
||||
//
|
||||
// and whichever are supplied, will be used for connecting to the
|
||||
// registry. The client cert and key are useful if you are
|
||||
// authenticating with a certificate; the CA cert is useful if
|
||||
// you are using a self-signed server certificate.
|
||||
// +optional
|
||||
CertSecretRef *meta.LocalObjectReference `json:"certSecretRef,omitempty"`
|
||||
|
||||
// The interval at which to check for image updates.
|
||||
// +required
|
||||
Interval metav1.Duration `json:"interval"`
|
||||
|
||||
// The timeout for remote OCI Repository operations like pulling, defaults to 60s.
|
||||
// +kubebuilder:default="60s"
|
||||
// +optional
|
||||
Timeout *metav1.Duration `json:"timeout,omitempty"`
|
||||
|
||||
// Ignore overrides the set of excluded patterns in the .sourceignore format
|
||||
// (which is the same as .gitignore). If not provided, a default will be used,
|
||||
// consult the documentation for your version to find out what those are.
|
||||
// +optional
|
||||
Ignore *string `json:"ignore,omitempty"`
|
||||
|
||||
// This flag tells the controller to suspend the reconciliation of this source.
|
||||
// +optional
|
||||
Suspend bool `json:"suspend,omitempty"`
|
||||
}
|
||||
|
||||
// OCIRepositoryRef defines the image reference for the OCIRepository's URL
|
||||
type OCIRepositoryRef struct {
|
||||
// Digest is the image digest to pull, takes precedence over SemVer.
|
||||
// The value should be in the format 'sha256:<HASH>'.
|
||||
// +optional
|
||||
Digest string `json:"digest,omitempty"`
|
||||
|
||||
// SemVer is the range of tags to pull selecting the latest within
|
||||
// the range, takes precedence over Tag.
|
||||
// +optional
|
||||
SemVer string `json:"semver,omitempty"`
|
||||
|
||||
// Tag is the image tag to pull, defaults to latest.
|
||||
// +optional
|
||||
Tag string `json:"tag,omitempty"`
|
||||
}
|
||||
|
||||
// OCIRepositoryVerification verifies the authenticity of an OCI Artifact
|
||||
type OCIRepositoryVerification struct {
|
||||
// Provider specifies the technology used to sign the OCI Artifact.
|
||||
// +kubebuilder:validation:Enum=cosign
|
||||
Provider string `json:"provider"`
|
||||
|
||||
// SecretRef specifies the Kubernetes Secret containing the
|
||||
// trusted public keys.
|
||||
SecretRef meta.LocalObjectReference `json:"secretRef"`
|
||||
}
|
||||
|
||||
// OCIRepositoryStatus defines the observed state of OCIRepository
|
||||
type OCIRepositoryStatus struct {
|
||||
// ObservedGeneration is the last observed generation.
|
||||
// +optional
|
||||
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
|
||||
|
||||
// Conditions holds the conditions for the OCIRepository.
|
||||
// +optional
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||
|
||||
// URL is the download link for the artifact output of the last OCI Repository sync.
|
||||
// +optional
|
||||
URL string `json:"url,omitempty"`
|
||||
|
||||
// Artifact represents the output of the last successful OCI Repository sync.
|
||||
// +optional
|
||||
Artifact *Artifact `json:"artifact,omitempty"`
|
||||
|
||||
meta.ReconcileRequestStatus `json:",inline"`
|
||||
}
|
||||
|
||||
const (
|
||||
// OCIPullFailedReason signals that a pull operation failed.
|
||||
OCIPullFailedReason string = "OCIArtifactPullFailed"
|
||||
|
||||
// OCILayerOperationFailedReason signals that an OCI layer operation failed.
|
||||
OCILayerOperationFailedReason string = "OCIArtifactLayerOperationFailed"
|
||||
)
|
||||
|
||||
// GetConditions returns the status conditions of the object.
|
||||
func (in OCIRepository) GetConditions() []metav1.Condition {
|
||||
return in.Status.Conditions
|
||||
}
|
||||
|
||||
// SetConditions sets the status conditions on the object.
|
||||
func (in *OCIRepository) SetConditions(conditions []metav1.Condition) {
|
||||
in.Status.Conditions = conditions
|
||||
}
|
||||
|
||||
// GetRequeueAfter returns the duration after which the OCIRepository must be
|
||||
// reconciled again.
|
||||
func (in OCIRepository) GetRequeueAfter() time.Duration {
|
||||
return in.Spec.Interval.Duration
|
||||
}
|
||||
|
||||
// GetArtifact returns the latest Artifact from the OCIRepository if present in
|
||||
// the status sub-resource.
|
||||
func (in *OCIRepository) GetArtifact() *Artifact {
|
||||
return in.Status.Artifact
|
||||
}
|
||||
|
||||
// +genclient
|
||||
// +genclient:Namespaced
|
||||
// +kubebuilder:storageversion
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:resource:shortName=ocirepo
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:printcolumn:name="URL",type=string,JSONPath=`.spec.url`
|
||||
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description=""
|
||||
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description=""
|
||||
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description=""
|
||||
|
||||
// OCIRepository is the Schema for the ocirepositories API
|
||||
type OCIRepository struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec OCIRepositorySpec `json:"spec,omitempty"`
|
||||
// +kubebuilder:default={"observedGeneration":-1}
|
||||
Status OCIRepositoryStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// OCIRepositoryList contains a list of OCIRepository
|
||||
// +kubebuilder:object:root=true
|
||||
type OCIRepositoryList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []OCIRepository `json:"items"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(&OCIRepository{}, &OCIRepositoryList{})
|
||||
}
|
|
@ -37,6 +37,13 @@ func (in *Artifact) DeepCopyInto(out *Artifact) {
|
|||
*out = new(int64)
|
||||
**out = **in
|
||||
}
|
||||
if in.Metadata != nil {
|
||||
in, out := &in.Metadata, &out.Metadata
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Artifact.
|
||||
|
@ -614,3 +621,162 @@ func (in *LocalHelmChartSourceReference) DeepCopy() *LocalHelmChartSourceReferen
|
|||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *OCIRepository) DeepCopyInto(out *OCIRepository) {
|
||||
*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 OCIRepository.
|
||||
func (in *OCIRepository) DeepCopy() *OCIRepository {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(OCIRepository)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *OCIRepository) 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 *OCIRepositoryList) DeepCopyInto(out *OCIRepositoryList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]OCIRepository, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OCIRepositoryList.
|
||||
func (in *OCIRepositoryList) DeepCopy() *OCIRepositoryList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(OCIRepositoryList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *OCIRepositoryList) 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 *OCIRepositoryRef) DeepCopyInto(out *OCIRepositoryRef) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OCIRepositoryRef.
|
||||
func (in *OCIRepositoryRef) DeepCopy() *OCIRepositoryRef {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(OCIRepositoryRef)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *OCIRepositorySpec) DeepCopyInto(out *OCIRepositorySpec) {
|
||||
*out = *in
|
||||
if in.Reference != nil {
|
||||
in, out := &in.Reference, &out.Reference
|
||||
*out = new(OCIRepositoryRef)
|
||||
**out = **in
|
||||
}
|
||||
if in.SecretRef != nil {
|
||||
in, out := &in.SecretRef, &out.SecretRef
|
||||
*out = new(meta.LocalObjectReference)
|
||||
**out = **in
|
||||
}
|
||||
if in.CertSecretRef != nil {
|
||||
in, out := &in.CertSecretRef, &out.CertSecretRef
|
||||
*out = new(meta.LocalObjectReference)
|
||||
**out = **in
|
||||
}
|
||||
out.Interval = in.Interval
|
||||
if in.Timeout != nil {
|
||||
in, out := &in.Timeout, &out.Timeout
|
||||
*out = new(v1.Duration)
|
||||
**out = **in
|
||||
}
|
||||
if in.Ignore != nil {
|
||||
in, out := &in.Ignore, &out.Ignore
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OCIRepositorySpec.
|
||||
func (in *OCIRepositorySpec) DeepCopy() *OCIRepositorySpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(OCIRepositorySpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *OCIRepositoryStatus) DeepCopyInto(out *OCIRepositoryStatus) {
|
||||
*out = *in
|
||||
if in.Conditions != nil {
|
||||
in, out := &in.Conditions, &out.Conditions
|
||||
*out = make([]v1.Condition, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.Artifact != nil {
|
||||
in, out := &in.Artifact, &out.Artifact
|
||||
*out = new(Artifact)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
out.ReconcileRequestStatus = in.ReconcileRequestStatus
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OCIRepositoryStatus.
|
||||
func (in *OCIRepositoryStatus) DeepCopy() *OCIRepositoryStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(OCIRepositoryStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *OCIRepositoryVerification) DeepCopyInto(out *OCIRepositoryVerification) {
|
||||
*out = *in
|
||||
out.SecretRef = in.SecretRef
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OCIRepositoryVerification.
|
||||
func (in *OCIRepositoryVerification) DeepCopy() *OCIRepositoryVerification {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(OCIRepositoryVerification)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
|
|
@ -384,6 +384,11 @@ spec:
|
|||
the last update of the Artifact.
|
||||
format: date-time
|
||||
type: string
|
||||
metadata:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Metadata holds upstream information such as OCI annotations.
|
||||
type: object
|
||||
path:
|
||||
description: Path is the relative file path of the Artifact. It
|
||||
can be used to locate the file in the root of the Artifact storage
|
||||
|
|
|
@ -559,6 +559,11 @@ spec:
|
|||
the last update of the Artifact.
|
||||
format: date-time
|
||||
type: string
|
||||
metadata:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Metadata holds upstream information such as OCI annotations.
|
||||
type: object
|
||||
path:
|
||||
description: Path is the relative file path of the Artifact. It
|
||||
can be used to locate the file in the root of the Artifact storage
|
||||
|
@ -677,6 +682,12 @@ spec:
|
|||
the last update of the Artifact.
|
||||
format: date-time
|
||||
type: string
|
||||
metadata:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Metadata holds upstream information such as OCI
|
||||
annotations.
|
||||
type: object
|
||||
path:
|
||||
description: Path is the relative file path of the Artifact.
|
||||
It can be used to locate the file in the root of the Artifact
|
||||
|
|
|
@ -432,6 +432,11 @@ spec:
|
|||
the last update of the Artifact.
|
||||
format: date-time
|
||||
type: string
|
||||
metadata:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Metadata holds upstream information such as OCI annotations.
|
||||
type: object
|
||||
path:
|
||||
description: Path is the relative file path of the Artifact. It
|
||||
can be used to locate the file in the root of the Artifact storage
|
||||
|
|
|
@ -362,6 +362,11 @@ spec:
|
|||
the last update of the Artifact.
|
||||
format: date-time
|
||||
type: string
|
||||
metadata:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Metadata holds upstream information such as OCI annotations.
|
||||
type: object
|
||||
path:
|
||||
description: Path is the relative file path of the Artifact. It
|
||||
can be used to locate the file in the root of the Artifact storage
|
||||
|
|
|
@ -0,0 +1,278 @@
|
|||
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.7.0
|
||||
creationTimestamp: null
|
||||
name: ocirepositories.source.toolkit.fluxcd.io
|
||||
spec:
|
||||
group: source.toolkit.fluxcd.io
|
||||
names:
|
||||
kind: OCIRepository
|
||||
listKind: OCIRepositoryList
|
||||
plural: ocirepositories
|
||||
shortNames:
|
||||
- ocirepo
|
||||
singular: ocirepository
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- additionalPrinterColumns:
|
||||
- jsonPath: .spec.url
|
||||
name: URL
|
||||
type: string
|
||||
- jsonPath: .status.conditions[?(@.type=="Ready")].status
|
||||
name: Ready
|
||||
type: string
|
||||
- jsonPath: .status.conditions[?(@.type=="Ready")].message
|
||||
name: Status
|
||||
type: string
|
||||
- jsonPath: .metadata.creationTimestamp
|
||||
name: Age
|
||||
type: date
|
||||
name: v1beta2
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: OCIRepository is the Schema for the ocirepositories API
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation
|
||||
of an object. Servers should convert recognized schemas to the latest
|
||||
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
type: string
|
||||
kind:
|
||||
description: 'Kind is a string value representing the REST resource this
|
||||
object represents. Servers may infer this from the endpoint the client
|
||||
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: OCIRepositorySpec defines the desired state of OCIRepository
|
||||
properties:
|
||||
certSecretRef:
|
||||
description: "CertSecretRef can be given the name of a secret containing
|
||||
either or both of \n - a PEM-encoded client certificate (`certFile`)
|
||||
and private key (`keyFile`); - a PEM-encoded CA certificate (`caFile`)
|
||||
\n and whichever are supplied, will be used for connecting to the
|
||||
\ registry. The client cert and key are useful if you are authenticating
|
||||
with a certificate; the CA cert is useful if you are using a self-signed
|
||||
server certificate."
|
||||
properties:
|
||||
name:
|
||||
description: Name of the referent.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
ignore:
|
||||
description: Ignore overrides the set of excluded patterns in the
|
||||
.sourceignore format (which is the same as .gitignore). If not provided,
|
||||
a default will be used, consult the documentation for your version
|
||||
to find out what those are.
|
||||
type: string
|
||||
interval:
|
||||
description: The interval at which to check for image updates.
|
||||
type: string
|
||||
provider:
|
||||
default: generic
|
||||
description: The provider used for authentication, can be 'aws', 'azure',
|
||||
'gcp' or 'generic'. When not specified, defaults to 'generic'.
|
||||
enum:
|
||||
- generic
|
||||
- aws
|
||||
- azure
|
||||
- gcp
|
||||
type: string
|
||||
ref:
|
||||
description: The OCI reference to pull and monitor for changes, defaults
|
||||
to the latest tag.
|
||||
properties:
|
||||
digest:
|
||||
description: Digest is the image digest to pull, takes precedence
|
||||
over SemVer. The value should be in the format 'sha256:<HASH>'.
|
||||
type: string
|
||||
semver:
|
||||
description: SemVer is the range of tags to pull selecting the
|
||||
latest within the range, takes precedence over Tag.
|
||||
type: string
|
||||
tag:
|
||||
description: Tag is the image tag to pull, defaults to latest.
|
||||
type: string
|
||||
type: object
|
||||
secretRef:
|
||||
description: SecretRef contains the secret name containing the registry
|
||||
login credentials to resolve image metadata. The secret must be
|
||||
of type kubernetes.io/dockerconfigjson.
|
||||
properties:
|
||||
name:
|
||||
description: Name of the referent.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
serviceAccountName:
|
||||
description: 'ServiceAccountName is the name of the Kubernetes ServiceAccount
|
||||
used to authenticate the image pull if the service account has attached
|
||||
pull secrets. For more information: https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#add-imagepullsecrets-to-a-service-account'
|
||||
type: string
|
||||
suspend:
|
||||
description: This flag tells the controller to suspend the reconciliation
|
||||
of this source.
|
||||
type: boolean
|
||||
timeout:
|
||||
default: 60s
|
||||
description: The timeout for remote OCI Repository operations like
|
||||
pulling, defaults to 60s.
|
||||
type: string
|
||||
url:
|
||||
description: URL is a reference to an OCI artifact repository hosted
|
||||
on a remote container registry.
|
||||
pattern: ^oci://.*$
|
||||
type: string
|
||||
required:
|
||||
- interval
|
||||
- url
|
||||
type: object
|
||||
status:
|
||||
default:
|
||||
observedGeneration: -1
|
||||
description: OCIRepositoryStatus defines the observed state of OCIRepository
|
||||
properties:
|
||||
artifact:
|
||||
description: Artifact represents the output of the last successful
|
||||
OCI Repository sync.
|
||||
properties:
|
||||
checksum:
|
||||
description: Checksum is the SHA256 checksum of the Artifact file.
|
||||
type: string
|
||||
lastUpdateTime:
|
||||
description: LastUpdateTime is the timestamp corresponding to
|
||||
the last update of the Artifact.
|
||||
format: date-time
|
||||
type: string
|
||||
metadata:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Metadata holds upstream information such as OCI annotations.
|
||||
type: object
|
||||
path:
|
||||
description: Path is the relative file path of the Artifact. It
|
||||
can be used to locate the file in the root of the Artifact storage
|
||||
on the local file system of the controller managing the Source.
|
||||
type: string
|
||||
revision:
|
||||
description: Revision is a human-readable identifier traceable
|
||||
in the origin source system. It can be a Git commit SHA, Git
|
||||
tag, a Helm chart version, etc.
|
||||
type: string
|
||||
size:
|
||||
description: Size is the number of bytes in the file.
|
||||
format: int64
|
||||
type: integer
|
||||
url:
|
||||
description: URL is the HTTP address of the Artifact as exposed
|
||||
by the controller managing the Source. It can be used to retrieve
|
||||
the Artifact for consumption, e.g. by another controller applying
|
||||
the Artifact contents.
|
||||
type: string
|
||||
required:
|
||||
- path
|
||||
- url
|
||||
type: object
|
||||
conditions:
|
||||
description: Conditions holds the conditions for the OCIRepository.
|
||||
items:
|
||||
description: "Condition contains details for one aspect of the current
|
||||
state of this API Resource. --- This struct is intended for direct
|
||||
use as an array at the field path .status.conditions. For example,
|
||||
type FooStatus struct{ // Represents the observations of a
|
||||
foo's current state. // Known .status.conditions.type are:
|
||||
\"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type
|
||||
\ // +patchStrategy=merge // +listType=map // +listMapKey=type
|
||||
\ Conditions []metav1.Condition `json:\"conditions,omitempty\"
|
||||
patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`
|
||||
\n // other fields }"
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
description: lastTransitionTime is the last time the condition
|
||||
transitioned from one status to another. This should be when
|
||||
the underlying condition changed. If that is not known, then
|
||||
using the time when the API field changed is acceptable.
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
description: message is a human readable message indicating
|
||||
details about the transition. This may be an empty string.
|
||||
maxLength: 32768
|
||||
type: string
|
||||
observedGeneration:
|
||||
description: observedGeneration represents the .metadata.generation
|
||||
that the condition was set based upon. For instance, if .metadata.generation
|
||||
is currently 12, but the .status.conditions[x].observedGeneration
|
||||
is 9, the condition is out of date with respect to the current
|
||||
state of the instance.
|
||||
format: int64
|
||||
minimum: 0
|
||||
type: integer
|
||||
reason:
|
||||
description: reason contains a programmatic identifier indicating
|
||||
the reason for the condition's last transition. Producers
|
||||
of specific condition types may define expected values and
|
||||
meanings for this field, and whether the values are considered
|
||||
a guaranteed API. The value should be a CamelCase string.
|
||||
This field may not be empty.
|
||||
maxLength: 1024
|
||||
minLength: 1
|
||||
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
|
||||
type: string
|
||||
status:
|
||||
description: status of the condition, one of True, False, Unknown.
|
||||
enum:
|
||||
- "True"
|
||||
- "False"
|
||||
- Unknown
|
||||
type: string
|
||||
type:
|
||||
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||
--- Many .condition.type values are consistent across resources
|
||||
like Available, but because arbitrary conditions can be useful
|
||||
(see .node.status.conditions), the ability to deconflict is
|
||||
important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
|
||||
maxLength: 316
|
||||
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
|
||||
type: string
|
||||
required:
|
||||
- lastTransitionTime
|
||||
- message
|
||||
- reason
|
||||
- status
|
||||
- type
|
||||
type: object
|
||||
type: array
|
||||
lastHandledReconcileAt:
|
||||
description: LastHandledReconcileAt holds the value of the most recent
|
||||
reconcile request value, so a change of the annotation value can
|
||||
be detected.
|
||||
type: string
|
||||
observedGeneration:
|
||||
description: ObservedGeneration is the last observed generation.
|
||||
format: int64
|
||||
type: integer
|
||||
url:
|
||||
description: URL is the download link for the artifact output of the
|
||||
last OCI Repository sync.
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
status:
|
||||
acceptedNames:
|
||||
kind: ""
|
||||
plural: ""
|
||||
conditions: []
|
||||
storedVersions: []
|
|
@ -5,4 +5,5 @@ resources:
|
|||
- bases/source.toolkit.fluxcd.io_helmrepositories.yaml
|
||||
- bases/source.toolkit.fluxcd.io_helmcharts.yaml
|
||||
- bases/source.toolkit.fluxcd.io_buckets.yaml
|
||||
- bases/source.toolkit.fluxcd.io_ocirepositories.yaml
|
||||
# +kubebuilder:scaffold:crdkustomizeresource
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
# permissions for end users to edit ocirepositories.
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: ocirepository-editor-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
- source.toolkit.fluxcd.io
|
||||
resources:
|
||||
- ocirepositories
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- source.toolkit.fluxcd.io
|
||||
resources:
|
||||
- ocirepositories/status
|
||||
verbs:
|
||||
- get
|
|
@ -0,0 +1,20 @@
|
|||
# permissions for end users to view ocirepositories.
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: ocirepository-viewer-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
- source.toolkit.fluxcd.io
|
||||
resources:
|
||||
- ocirepositories
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- source.toolkit.fluxcd.io
|
||||
resources:
|
||||
- ocirepositories/status
|
||||
verbs:
|
||||
- get
|
|
@ -141,3 +141,33 @@ rules:
|
|||
- get
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- source.toolkit.fluxcd.io
|
||||
resources:
|
||||
- ocirepositories
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- source.toolkit.fluxcd.io
|
||||
resources:
|
||||
- ocirepositories/finalizers
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- source.toolkit.fluxcd.io
|
||||
resources:
|
||||
- ocirepositories/status
|
||||
verbs:
|
||||
- get
|
||||
- patch
|
||||
- update
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
apiVersion: source.toolkit.fluxcd.io/v1beta2
|
||||
kind: OCIRepository
|
||||
metadata:
|
||||
name: ocirepository-sample
|
||||
spec:
|
||||
interval: 1m
|
||||
url: oci://ghcr.io/stefanprodan/manifests/podinfo
|
||||
ref:
|
||||
tag: 6.1.6
|
|
@ -0,0 +1,909 @@
|
|||
/*
|
||||
Copyright 2022 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
"github.com/google/go-containerregistry/pkg/authn/k8schain"
|
||||
"github.com/google/go-containerregistry/pkg/crane"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/uuid"
|
||||
kuberecorder "k8s.io/client-go/tools/record"
|
||||
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/builder"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||
"sigs.k8s.io/controller-runtime/pkg/ratelimiter"
|
||||
|
||||
"github.com/fluxcd/pkg/apis/meta"
|
||||
"github.com/fluxcd/pkg/oci"
|
||||
"github.com/fluxcd/pkg/oci/auth/login"
|
||||
"github.com/fluxcd/pkg/runtime/conditions"
|
||||
helper "github.com/fluxcd/pkg/runtime/controller"
|
||||
"github.com/fluxcd/pkg/runtime/events"
|
||||
"github.com/fluxcd/pkg/runtime/patch"
|
||||
"github.com/fluxcd/pkg/runtime/predicates"
|
||||
"github.com/fluxcd/pkg/untar"
|
||||
"github.com/fluxcd/pkg/version"
|
||||
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
|
||||
serror "github.com/fluxcd/source-controller/internal/error"
|
||||
sreconcile "github.com/fluxcd/source-controller/internal/reconcile"
|
||||
"github.com/fluxcd/source-controller/internal/reconcile/summarize"
|
||||
"github.com/fluxcd/source-controller/internal/util"
|
||||
)
|
||||
|
||||
// ociRepositoryReadyCondition contains the information required to summarize a
|
||||
// v1beta2.OCIRepository Ready Condition.
|
||||
var ociRepositoryReadyCondition = summarize.Conditions{
|
||||
Target: meta.ReadyCondition,
|
||||
Owned: []string{
|
||||
sourcev1.StorageOperationFailedCondition,
|
||||
sourcev1.FetchFailedCondition,
|
||||
sourcev1.ArtifactOutdatedCondition,
|
||||
sourcev1.ArtifactInStorageCondition,
|
||||
meta.ReadyCondition,
|
||||
meta.ReconcilingCondition,
|
||||
meta.StalledCondition,
|
||||
},
|
||||
Summarize: []string{
|
||||
sourcev1.StorageOperationFailedCondition,
|
||||
sourcev1.FetchFailedCondition,
|
||||
sourcev1.ArtifactOutdatedCondition,
|
||||
sourcev1.ArtifactInStorageCondition,
|
||||
meta.StalledCondition,
|
||||
meta.ReconcilingCondition,
|
||||
},
|
||||
NegativePolarity: []string{
|
||||
sourcev1.StorageOperationFailedCondition,
|
||||
sourcev1.FetchFailedCondition,
|
||||
sourcev1.ArtifactOutdatedCondition,
|
||||
meta.StalledCondition,
|
||||
meta.ReconcilingCondition,
|
||||
},
|
||||
}
|
||||
|
||||
// ociRepositoryFailConditions contains the conditions that represent a failure.
|
||||
var ociRepositoryFailConditions = []string{
|
||||
sourcev1.FetchFailedCondition,
|
||||
sourcev1.StorageOperationFailedCondition,
|
||||
}
|
||||
|
||||
type invalidOCIURLError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (e invalidOCIURLError) Error() string {
|
||||
return e.err.Error()
|
||||
}
|
||||
|
||||
// ociRepositoryReconcileFunc is the function type for all the v1beta2.OCIRepository
|
||||
// (sub)reconcile functions. The type implementations are grouped and
|
||||
// executed serially to perform the complete reconcile of the object.
|
||||
type ociRepositoryReconcileFunc func(ctx context.Context, obj *sourcev1.OCIRepository, metadata *sourcev1.Artifact, dir string) (sreconcile.Result, error)
|
||||
|
||||
// OCIRepositoryReconciler reconciles a v1beta2.OCIRepository object
|
||||
type OCIRepositoryReconciler struct {
|
||||
client.Client
|
||||
helper.Metrics
|
||||
kuberecorder.EventRecorder
|
||||
|
||||
Storage *Storage
|
||||
ControllerName string
|
||||
requeueDependency time.Duration
|
||||
}
|
||||
|
||||
type OCIRepositoryReconcilerOptions struct {
|
||||
MaxConcurrentReconciles int
|
||||
DependencyRequeueInterval time.Duration
|
||||
RateLimiter ratelimiter.RateLimiter
|
||||
}
|
||||
|
||||
// SetupWithManager sets up the controller with the Manager.
|
||||
func (r *OCIRepositoryReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return r.SetupWithManagerAndOptions(mgr, OCIRepositoryReconcilerOptions{})
|
||||
}
|
||||
|
||||
func (r *OCIRepositoryReconciler) SetupWithManagerAndOptions(mgr ctrl.Manager, opts OCIRepositoryReconcilerOptions) error {
|
||||
r.requeueDependency = opts.DependencyRequeueInterval
|
||||
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&sourcev1.OCIRepository{}, builder.WithPredicates(
|
||||
predicate.Or(predicate.GenerationChangedPredicate{}, predicates.ReconcileRequestedPredicate{}),
|
||||
)).
|
||||
WithOptions(controller.Options{
|
||||
MaxConcurrentReconciles: opts.MaxConcurrentReconciles,
|
||||
RateLimiter: opts.RateLimiter,
|
||||
}).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
// +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=ocirepositories,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=ocirepositories/status,verbs=get;update;patch
|
||||
// +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=ocirepositories/finalizers,verbs=get;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch
|
||||
|
||||
func (r *OCIRepositoryReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, retErr error) {
|
||||
start := time.Now()
|
||||
log := ctrl.LoggerFrom(ctx).
|
||||
// Sets a reconcile ID to correlate logs from all suboperations.
|
||||
WithValues("reconcileID", uuid.NewUUID())
|
||||
|
||||
// logger will be associated to the new context that is
|
||||
// returned from ctrl.LoggerInto.
|
||||
ctx = ctrl.LoggerInto(ctx, log)
|
||||
|
||||
// Fetch the OCIRepository
|
||||
obj := &sourcev1.OCIRepository{}
|
||||
if err := r.Get(ctx, req.NamespacedName, obj); err != nil {
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
|
||||
// Record suspended status metric
|
||||
r.RecordSuspend(ctx, obj, obj.Spec.Suspend)
|
||||
|
||||
// Return early if the object is suspended
|
||||
if obj.Spec.Suspend {
|
||||
log.Info("reconciliation is suspended for this object")
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// Initialize the patch helper with the current version of the object.
|
||||
patchHelper, err := patch.NewHelper(obj, r.Client)
|
||||
if err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// recResult stores the abstracted reconcile result.
|
||||
var recResult sreconcile.Result
|
||||
|
||||
// Always attempt to patch the object and status after each reconciliation
|
||||
// NOTE: The final runtime result and error are set in this block.
|
||||
defer func() {
|
||||
summarizeHelper := summarize.NewHelper(r.EventRecorder, patchHelper)
|
||||
summarizeOpts := []summarize.Option{
|
||||
summarize.WithConditions(ociRepositoryReadyCondition),
|
||||
summarize.WithReconcileResult(recResult),
|
||||
summarize.WithReconcileError(retErr),
|
||||
summarize.WithIgnoreNotFound(),
|
||||
summarize.WithProcessors(
|
||||
summarize.ErrorActionHandler,
|
||||
summarize.RecordReconcileReq,
|
||||
),
|
||||
summarize.WithResultBuilder(sreconcile.AlwaysRequeueResultBuilder{RequeueAfter: obj.GetRequeueAfter()}),
|
||||
summarize.WithPatchFieldOwner(r.ControllerName),
|
||||
}
|
||||
result, retErr = summarizeHelper.SummarizeAndPatch(ctx, obj, summarizeOpts...)
|
||||
|
||||
// Always record readiness and duration metrics
|
||||
r.Metrics.RecordReadiness(ctx, obj)
|
||||
r.Metrics.RecordDuration(ctx, obj, start)
|
||||
}()
|
||||
|
||||
// Add finalizer first if not exist to avoid the race condition between init and delete
|
||||
if !controllerutil.ContainsFinalizer(obj, sourcev1.SourceFinalizer) {
|
||||
controllerutil.AddFinalizer(obj, sourcev1.SourceFinalizer)
|
||||
recResult = sreconcile.ResultRequeue
|
||||
return
|
||||
}
|
||||
|
||||
// Examine if the object is under deletion
|
||||
if !obj.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||
recResult, retErr = r.reconcileDelete(ctx, obj)
|
||||
return
|
||||
}
|
||||
|
||||
// Reconcile actual object
|
||||
reconcilers := []ociRepositoryReconcileFunc{
|
||||
r.reconcileStorage,
|
||||
r.reconcileSource,
|
||||
r.reconcileArtifact,
|
||||
}
|
||||
recResult, retErr = r.reconcile(ctx, obj, reconcilers)
|
||||
return
|
||||
}
|
||||
|
||||
// reconcile iterates through the ociRepositoryReconcileFunc tasks for the
|
||||
// object. It returns early on the first call that returns
|
||||
// reconcile.ResultRequeue, or produces an error.
|
||||
func (r *OCIRepositoryReconciler) reconcile(ctx context.Context, obj *sourcev1.OCIRepository, reconcilers []ociRepositoryReconcileFunc) (sreconcile.Result, error) {
|
||||
oldObj := obj.DeepCopy()
|
||||
|
||||
// Mark as reconciling if generation differs.
|
||||
if obj.Generation != obj.Status.ObservedGeneration {
|
||||
conditions.MarkReconciling(obj, "NewGeneration", "reconciling new object generation (%d)", obj.Generation)
|
||||
}
|
||||
|
||||
// Create temp working dir
|
||||
tmpDir, err := util.TempDirForObj("", obj)
|
||||
if err != nil {
|
||||
e := serror.NewGeneric(
|
||||
fmt.Errorf("failed to create temporary working directory: %w", err),
|
||||
sourcev1.DirCreationFailedReason,
|
||||
)
|
||||
conditions.MarkTrue(obj, sourcev1.StorageOperationFailedCondition, e.Reason, e.Err.Error())
|
||||
return sreconcile.ResultEmpty, e
|
||||
}
|
||||
defer func() {
|
||||
if err = os.RemoveAll(tmpDir); err != nil {
|
||||
ctrl.LoggerFrom(ctx).Error(err, "failed to remove temporary working directory")
|
||||
}
|
||||
}()
|
||||
conditions.Delete(obj, sourcev1.StorageOperationFailedCondition)
|
||||
|
||||
var (
|
||||
res sreconcile.Result
|
||||
resErr error
|
||||
metadata = sourcev1.Artifact{}
|
||||
)
|
||||
|
||||
// Run the sub-reconcilers and build the result of reconciliation.
|
||||
for _, rec := range reconcilers {
|
||||
recResult, err := rec(ctx, obj, &metadata, tmpDir)
|
||||
// Exit immediately on ResultRequeue.
|
||||
if recResult == sreconcile.ResultRequeue {
|
||||
return sreconcile.ResultRequeue, nil
|
||||
}
|
||||
// If an error is received, prioritize the returned results because an
|
||||
// error also means immediate requeue.
|
||||
if err != nil {
|
||||
resErr = err
|
||||
res = recResult
|
||||
break
|
||||
}
|
||||
// Prioritize requeue request in the result.
|
||||
res = sreconcile.LowestRequeuingResult(res, recResult)
|
||||
}
|
||||
|
||||
r.notify(ctx, oldObj, obj, res, resErr)
|
||||
|
||||
return res, resErr
|
||||
}
|
||||
|
||||
// reconcileSource fetches the upstream OCI artifact metadata and content.
|
||||
// If this fails, it records v1beta2.FetchFailedCondition=True on the object and returns early.
|
||||
func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sourcev1.OCIRepository, metadata *sourcev1.Artifact, dir string) (sreconcile.Result, error) {
|
||||
ctxTimeout, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration)
|
||||
defer cancel()
|
||||
|
||||
options := r.craneOptions(ctxTimeout)
|
||||
|
||||
// Generate the registry credential keychain either from static credentials or using cloud OIDC
|
||||
keychain, err := r.keychain(ctx, obj)
|
||||
if err != nil {
|
||||
e := serror.NewGeneric(
|
||||
fmt.Errorf("failed to get credential: %w", err),
|
||||
sourcev1.AuthenticationFailedReason,
|
||||
)
|
||||
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
||||
return sreconcile.ResultEmpty, e
|
||||
}
|
||||
options = append(options, crane.WithAuthFromKeychain(keychain))
|
||||
|
||||
if obj.Spec.Provider != sourcev1.GenericOCIProvider {
|
||||
auth, authErr := r.oidcAuth(ctxTimeout, obj)
|
||||
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
|
||||
e := serror.NewGeneric(
|
||||
fmt.Errorf("failed to get credential from %s: %w", obj.Spec.Provider, authErr),
|
||||
sourcev1.AuthenticationFailedReason,
|
||||
)
|
||||
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
||||
return sreconcile.ResultEmpty, e
|
||||
}
|
||||
if auth != nil {
|
||||
options = append(options, crane.WithAuth(auth))
|
||||
}
|
||||
}
|
||||
|
||||
// Generate the transport for remote operations
|
||||
transport, err := r.transport(ctx, obj)
|
||||
if err != nil {
|
||||
e := serror.NewGeneric(
|
||||
fmt.Errorf("failed to generate transport for '%s': %w", obj.Spec.URL, err),
|
||||
sourcev1.AuthenticationFailedReason,
|
||||
)
|
||||
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
||||
return sreconcile.ResultEmpty, e
|
||||
}
|
||||
if transport != nil {
|
||||
options = append(options, crane.WithTransport(transport))
|
||||
}
|
||||
|
||||
// Determine which artifact revision to pull
|
||||
url, err := r.getArtifactURL(obj, options)
|
||||
if err != nil {
|
||||
if _, ok := err.(invalidOCIURLError); ok {
|
||||
e := serror.NewStalling(
|
||||
fmt.Errorf("URL validation failed for '%s': %w", obj.Spec.URL, err),
|
||||
sourcev1.URLInvalidReason)
|
||||
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
||||
return sreconcile.ResultEmpty, e
|
||||
}
|
||||
|
||||
e := serror.NewGeneric(
|
||||
fmt.Errorf("failed to determine the artifact tag for '%s': %w", obj.Spec.URL, err),
|
||||
sourcev1.ReadOperationFailedReason)
|
||||
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
||||
return sreconcile.ResultEmpty, e
|
||||
}
|
||||
|
||||
// Pull artifact from the remote container registry
|
||||
img, err := crane.Pull(url, options...)
|
||||
if err != nil {
|
||||
e := serror.NewGeneric(
|
||||
fmt.Errorf("failed to pull artifact from '%s': %w", obj.Spec.URL, err),
|
||||
sourcev1.OCIPullFailedReason,
|
||||
)
|
||||
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
||||
return sreconcile.ResultEmpty, e
|
||||
}
|
||||
|
||||
// Determine the artifact SHA256 digest
|
||||
imgDigest, err := img.Digest()
|
||||
if err != nil {
|
||||
e := serror.NewGeneric(
|
||||
fmt.Errorf("failed to determine artifact digest: %w", err),
|
||||
sourcev1.OCILayerOperationFailedReason,
|
||||
)
|
||||
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
||||
return sreconcile.ResultEmpty, e
|
||||
}
|
||||
|
||||
// Set the internal revision to the remote digest hex
|
||||
revision := imgDigest.Hex
|
||||
|
||||
// Copy the OCI annotations to the internal artifact metadata
|
||||
manifest, err := img.Manifest()
|
||||
if err != nil {
|
||||
e := serror.NewGeneric(
|
||||
fmt.Errorf("failed to parse artifact manifest: %w", err),
|
||||
sourcev1.OCILayerOperationFailedReason,
|
||||
)
|
||||
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
||||
return sreconcile.ResultEmpty, e
|
||||
}
|
||||
|
||||
m := &sourcev1.Artifact{
|
||||
Revision: revision,
|
||||
Metadata: manifest.Annotations,
|
||||
}
|
||||
m.DeepCopyInto(metadata)
|
||||
|
||||
// Mark observations about the revision on the object
|
||||
defer func() {
|
||||
if !obj.GetArtifact().HasRevision(revision) {
|
||||
message := fmt.Sprintf("new digest '%s' for '%s'", revision, url)
|
||||
conditions.MarkTrue(obj, sourcev1.ArtifactOutdatedCondition, "NewRevision", message)
|
||||
conditions.MarkReconciling(obj, "NewRevision", message)
|
||||
}
|
||||
}()
|
||||
|
||||
// Extract the content of the first artifact layer
|
||||
if !obj.GetArtifact().HasRevision(revision) {
|
||||
layers, err := img.Layers()
|
||||
if err != nil {
|
||||
e := serror.NewGeneric(
|
||||
fmt.Errorf("failed to parse artifact layers: %w", err),
|
||||
sourcev1.OCILayerOperationFailedReason,
|
||||
)
|
||||
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
||||
return sreconcile.ResultEmpty, e
|
||||
}
|
||||
|
||||
if len(layers) < 1 {
|
||||
e := serror.NewGeneric(
|
||||
fmt.Errorf("no layers found in artifact"),
|
||||
sourcev1.OCILayerOperationFailedReason,
|
||||
)
|
||||
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
||||
return sreconcile.ResultEmpty, e
|
||||
}
|
||||
|
||||
blob, err := layers[0].Compressed()
|
||||
if err != nil {
|
||||
e := serror.NewGeneric(
|
||||
fmt.Errorf("failed to extract the first layer from artifact: %w", err),
|
||||
sourcev1.OCILayerOperationFailedReason,
|
||||
)
|
||||
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
||||
return sreconcile.ResultEmpty, e
|
||||
}
|
||||
|
||||
if _, err = untar.Untar(blob, dir); err != nil {
|
||||
e := serror.NewGeneric(
|
||||
fmt.Errorf("failed to untar the first layer from artifact: %w", err),
|
||||
sourcev1.OCILayerOperationFailedReason,
|
||||
)
|
||||
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
||||
return sreconcile.ResultEmpty, e
|
||||
}
|
||||
}
|
||||
|
||||
conditions.Delete(obj, sourcev1.FetchFailedCondition)
|
||||
return sreconcile.ResultSuccess, nil
|
||||
}
|
||||
|
||||
// parseRepositoryURL validates and extracts the repository URL.
|
||||
func (r *OCIRepositoryReconciler) parseRepositoryURL(obj *sourcev1.OCIRepository) (string, error) {
|
||||
if !strings.HasPrefix(obj.Spec.URL, sourcev1.OCIRepositoryPrefix) {
|
||||
return "", fmt.Errorf("URL must be in format 'oci://<domain>/<org>/<repo>'")
|
||||
}
|
||||
|
||||
url := strings.TrimPrefix(obj.Spec.URL, sourcev1.OCIRepositoryPrefix)
|
||||
ref, err := name.ParseReference(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
imageName := strings.TrimPrefix(url, ref.Context().RegistryStr())
|
||||
if s := strings.Split(imageName, ":"); len(s) > 1 {
|
||||
return "", fmt.Errorf("URL must not contain a tag; remove ':%s'", s[1])
|
||||
}
|
||||
|
||||
return ref.Context().Name(), nil
|
||||
}
|
||||
|
||||
// getArtifactURL determines which tag or digest should be used and returns the OCI artifact FQN.
|
||||
func (r *OCIRepositoryReconciler) getArtifactURL(obj *sourcev1.OCIRepository, options []crane.Option) (string, error) {
|
||||
url, err := r.parseRepositoryURL(obj)
|
||||
if err != nil {
|
||||
return "", invalidOCIURLError{err}
|
||||
}
|
||||
|
||||
if obj.Spec.Reference != nil {
|
||||
if obj.Spec.Reference.Digest != "" {
|
||||
return fmt.Sprintf("%s@%s", url, obj.Spec.Reference.Digest), nil
|
||||
}
|
||||
|
||||
if obj.Spec.Reference.SemVer != "" {
|
||||
tag, err := r.getTagBySemver(url, obj.Spec.Reference.SemVer, options)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%s:%s", url, tag), nil
|
||||
}
|
||||
|
||||
if obj.Spec.Reference.Tag != "" {
|
||||
return fmt.Sprintf("%s:%s", url, obj.Spec.Reference.Tag), nil
|
||||
}
|
||||
}
|
||||
|
||||
return url, nil
|
||||
}
|
||||
|
||||
// getTagBySemver call the remote container registry, fetches all the tags from the repository,
|
||||
// and returns the latest tag according to the semver expression.
|
||||
func (r *OCIRepositoryReconciler) getTagBySemver(url, exp string, options []crane.Option) (string, error) {
|
||||
tags, err := crane.ListTags(url, options...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
constraint, err := semver.NewConstraint(exp)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("semver '%s' parse error: %w", exp, err)
|
||||
}
|
||||
|
||||
var matchingVersions []*semver.Version
|
||||
for _, t := range tags {
|
||||
v, err := version.ParseVersion(t)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if constraint.Check(v) {
|
||||
matchingVersions = append(matchingVersions, v)
|
||||
}
|
||||
}
|
||||
|
||||
if len(matchingVersions) == 0 {
|
||||
return "", fmt.Errorf("no match found for semver: %s", exp)
|
||||
}
|
||||
|
||||
sort.Sort(sort.Reverse(semver.Collection(matchingVersions)))
|
||||
return matchingVersions[0].Original(), nil
|
||||
}
|
||||
|
||||
// keychain generates the credential keychain based on the resource
|
||||
// configuration. If no auth is specified a default keychain with
|
||||
// anonymous access is returned
|
||||
func (r *OCIRepositoryReconciler) keychain(ctx context.Context, obj *sourcev1.OCIRepository) (authn.Keychain, error) {
|
||||
pullSecretNames := sets.NewString()
|
||||
|
||||
// lookup auth secret
|
||||
if obj.Spec.SecretRef != nil {
|
||||
pullSecretNames.Insert(obj.Spec.SecretRef.Name)
|
||||
}
|
||||
|
||||
// lookup service account
|
||||
if obj.Spec.ServiceAccountName != "" {
|
||||
serviceAccountName := obj.Spec.ServiceAccountName
|
||||
serviceAccount := corev1.ServiceAccount{}
|
||||
err := r.Get(ctx, types.NamespacedName{Namespace: obj.Namespace, Name: serviceAccountName}, &serviceAccount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, ips := range serviceAccount.ImagePullSecrets {
|
||||
pullSecretNames.Insert(ips.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// if no pullsecrets available return DefaultKeyChain
|
||||
if len(pullSecretNames) == 0 {
|
||||
return authn.DefaultKeychain, nil
|
||||
}
|
||||
|
||||
// lookup image pull secrets
|
||||
imagePullSecrets := make([]corev1.Secret, len(pullSecretNames))
|
||||
for i, imagePullSecretName := range pullSecretNames.List() {
|
||||
imagePullSecret := corev1.Secret{}
|
||||
err := r.Get(ctx, types.NamespacedName{Namespace: obj.Namespace, Name: imagePullSecretName}, &imagePullSecret)
|
||||
if err != nil {
|
||||
r.eventLogf(ctx, obj, events.EventSeverityTrace, sourcev1.AuthenticationFailedReason,
|
||||
"auth secret '%s' not found", imagePullSecretName)
|
||||
return nil, err
|
||||
}
|
||||
imagePullSecrets[i] = imagePullSecret
|
||||
}
|
||||
|
||||
return k8schain.NewFromPullSecrets(ctx, imagePullSecrets)
|
||||
}
|
||||
|
||||
// transport clones the default transport from remote and when a certSecretRef is specified,
|
||||
// the returned transport will include the TLS client and/or CA certificates.
|
||||
func (r *OCIRepositoryReconciler) transport(ctx context.Context, obj *sourcev1.OCIRepository) (http.RoundTripper, error) {
|
||||
if obj.Spec.CertSecretRef == nil || obj.Spec.CertSecretRef.Name == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
certSecretName := types.NamespacedName{
|
||||
Namespace: obj.Namespace,
|
||||
Name: obj.Spec.CertSecretRef.Name,
|
||||
}
|
||||
var certSecret corev1.Secret
|
||||
if err := r.Get(ctx, certSecretName, &certSecret); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
transport := remote.DefaultTransport.Clone()
|
||||
tlsConfig := transport.TLSClientConfig
|
||||
|
||||
if clientCert, ok := certSecret.Data[oci.ClientCert]; ok {
|
||||
// parse and set client cert and secret
|
||||
if clientKey, ok := certSecret.Data[oci.ClientKey]; ok {
|
||||
cert, err := tls.X509KeyPair(clientCert, clientKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConfig.Certificates = append(tlsConfig.Certificates, cert)
|
||||
} else {
|
||||
return nil, fmt.Errorf("'%s' found in secret, but no %s", oci.ClientCert, oci.ClientKey)
|
||||
}
|
||||
}
|
||||
|
||||
if caCert, ok := certSecret.Data[oci.CACert]; ok {
|
||||
syscerts, err := x509.SystemCertPool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
syscerts.AppendCertsFromPEM(caCert)
|
||||
tlsConfig.RootCAs = syscerts
|
||||
}
|
||||
return transport, nil
|
||||
|
||||
}
|
||||
|
||||
// oidcAuth generates the OIDC credential authenticator based on the specified cloud provider.
|
||||
func (r *OCIRepositoryReconciler) oidcAuth(ctx context.Context, obj *sourcev1.OCIRepository) (authn.Authenticator, error) {
|
||||
url := strings.TrimPrefix(obj.Spec.URL, sourcev1.OCIRepositoryPrefix)
|
||||
ref, err := name.ParseReference(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse URL '%s': %w", obj.Spec.URL, err)
|
||||
}
|
||||
|
||||
opts := login.ProviderOptions{}
|
||||
switch obj.Spec.Provider {
|
||||
case sourcev1.AmazonOCIProvider:
|
||||
opts.AwsAutoLogin = true
|
||||
case sourcev1.AzureOCIProvider:
|
||||
opts.AzureAutoLogin = true
|
||||
case sourcev1.GoogleOCIProvider:
|
||||
opts.GcpAutoLogin = true
|
||||
}
|
||||
|
||||
return login.NewManager().Login(ctx, url, ref, opts)
|
||||
}
|
||||
|
||||
// craneOptions sets the auth headers, timeout and user agent
|
||||
// for all operations against remote container registries.
|
||||
func (r *OCIRepositoryReconciler) craneOptions(ctx context.Context) []crane.Option {
|
||||
options := []crane.Option{
|
||||
crane.WithContext(ctx),
|
||||
crane.WithUserAgent(oci.UserAgent),
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
// reconcileStorage ensures the current state of the storage matches the
|
||||
// desired and previously observed state.
|
||||
//
|
||||
// The garbage collection is executed based on the flag configured settings and
|
||||
// may remove files that are beyond their TTL or the maximum number of files
|
||||
// to survive a collection cycle.
|
||||
// If the Artifact in the Status of the object disappeared from the Storage,
|
||||
// it is removed from the object.
|
||||
// If the object does not have an Artifact in its Status, a Reconciling
|
||||
// condition is added.
|
||||
// The hostname of any URL in the Status of the object are updated, to ensure
|
||||
// they match the Storage server hostname of current runtime.
|
||||
func (r *OCIRepositoryReconciler) reconcileStorage(ctx context.Context,
|
||||
obj *sourcev1.OCIRepository, _ *sourcev1.Artifact, _ string) (sreconcile.Result, error) {
|
||||
// Garbage collect previous advertised artifact(s) from storage
|
||||
_ = r.garbageCollect(ctx, obj)
|
||||
|
||||
// Determine if the advertised artifact is still in storage
|
||||
if artifact := obj.GetArtifact(); artifact != nil && !r.Storage.ArtifactExist(*artifact) {
|
||||
obj.Status.Artifact = nil
|
||||
obj.Status.URL = ""
|
||||
// Remove the condition as the artifact doesn't exist.
|
||||
conditions.Delete(obj, sourcev1.ArtifactInStorageCondition)
|
||||
}
|
||||
|
||||
// Record that we do not have an artifact
|
||||
if obj.GetArtifact() == nil {
|
||||
conditions.MarkReconciling(obj, "NoArtifact", "no artifact for resource in storage")
|
||||
conditions.Delete(obj, sourcev1.ArtifactInStorageCondition)
|
||||
return sreconcile.ResultSuccess, nil
|
||||
}
|
||||
|
||||
// Always update URLs to ensure hostname is up-to-date
|
||||
r.Storage.SetArtifactURL(obj.GetArtifact())
|
||||
obj.Status.URL = r.Storage.SetHostname(obj.Status.URL)
|
||||
|
||||
return sreconcile.ResultSuccess, nil
|
||||
}
|
||||
|
||||
// reconcileArtifact archives a new Artifact to the Storage, if the current
|
||||
// (Status) data on the object does not match the given.
|
||||
//
|
||||
// The inspection of the given data to the object is differed, ensuring any
|
||||
// stale observations like v1beta2.ArtifactOutdatedCondition are removed.
|
||||
// If the given Artifact does not differ from the object's current, it returns
|
||||
// early.
|
||||
// On a successful archive, the Artifact in the Status of the object is set,
|
||||
// and the symlink in the Storage is updated to its path.
|
||||
func (r *OCIRepositoryReconciler) reconcileArtifact(ctx context.Context,
|
||||
obj *sourcev1.OCIRepository, metadata *sourcev1.Artifact, dir string) (sreconcile.Result, error) {
|
||||
// Calculate revision
|
||||
revision := metadata.Revision
|
||||
|
||||
// Create artifact
|
||||
artifact := r.Storage.NewArtifactFor(obj.Kind, obj, revision, fmt.Sprintf("%s.tar.gz", revision))
|
||||
|
||||
// Set the ArtifactInStorageCondition if there's no drift.
|
||||
defer func() {
|
||||
if obj.GetArtifact().HasRevision(artifact.Revision) {
|
||||
conditions.Delete(obj, sourcev1.ArtifactOutdatedCondition)
|
||||
conditions.MarkTrue(obj, sourcev1.ArtifactInStorageCondition, meta.SucceededReason,
|
||||
"stored artifact for digest '%s'", artifact.Revision)
|
||||
}
|
||||
}()
|
||||
|
||||
// The artifact is up-to-date
|
||||
if obj.GetArtifact().HasRevision(artifact.Revision) {
|
||||
r.eventLogf(ctx, obj, events.EventTypeTrace, sourcev1.ArtifactUpToDateReason,
|
||||
"artifact up-to-date with remote digest: '%s'", artifact.Revision)
|
||||
return sreconcile.ResultSuccess, nil
|
||||
}
|
||||
|
||||
// Ensure target path exists and is a directory
|
||||
if f, err := os.Stat(dir); err != nil {
|
||||
e := serror.NewGeneric(
|
||||
fmt.Errorf("failed to stat source path: %w", err),
|
||||
sourcev1.StatOperationFailedReason,
|
||||
)
|
||||
conditions.MarkTrue(obj, sourcev1.StorageOperationFailedCondition, e.Reason, e.Err.Error())
|
||||
return sreconcile.ResultEmpty, e
|
||||
} else if !f.IsDir() {
|
||||
e := serror.NewGeneric(
|
||||
fmt.Errorf("source path '%s' is not a directory", dir),
|
||||
sourcev1.InvalidPathReason,
|
||||
)
|
||||
conditions.MarkTrue(obj, sourcev1.StorageOperationFailedCondition, e.Reason, e.Err.Error())
|
||||
return sreconcile.ResultEmpty, e
|
||||
}
|
||||
|
||||
// Ensure artifact directory exists and acquire lock
|
||||
if err := r.Storage.MkdirAll(artifact); err != nil {
|
||||
e := serror.NewGeneric(
|
||||
fmt.Errorf("failed to create artifact directory: %w", err),
|
||||
sourcev1.DirCreationFailedReason,
|
||||
)
|
||||
conditions.MarkTrue(obj, sourcev1.StorageOperationFailedCondition, e.Reason, e.Err.Error())
|
||||
return sreconcile.ResultEmpty, e
|
||||
}
|
||||
unlock, err := r.Storage.Lock(artifact)
|
||||
if err != nil {
|
||||
return sreconcile.ResultEmpty, serror.NewGeneric(
|
||||
fmt.Errorf("failed to acquire lock for artifact: %w", err),
|
||||
meta.FailedReason,
|
||||
)
|
||||
}
|
||||
defer unlock()
|
||||
|
||||
// Archive directory to storage
|
||||
if err := r.Storage.Archive(&artifact, dir, nil); err != nil {
|
||||
e := serror.NewGeneric(
|
||||
fmt.Errorf("unable to archive artifact to storage: %s", err),
|
||||
sourcev1.ArchiveOperationFailedReason,
|
||||
)
|
||||
conditions.MarkTrue(obj, sourcev1.StorageOperationFailedCondition, e.Reason, e.Err.Error())
|
||||
return sreconcile.ResultEmpty, e
|
||||
}
|
||||
|
||||
// Record it on the object
|
||||
obj.Status.Artifact = artifact.DeepCopy()
|
||||
obj.Status.Artifact.Metadata = metadata.Metadata
|
||||
|
||||
// Update symlink on a "best effort" basis
|
||||
url, err := r.Storage.Symlink(artifact, "latest.tar.gz")
|
||||
if err != nil {
|
||||
r.eventLogf(ctx, obj, events.EventTypeTrace, sourcev1.SymlinkUpdateFailedReason,
|
||||
"failed to update status URL symlink: %s", err)
|
||||
}
|
||||
if url != "" {
|
||||
obj.Status.URL = url
|
||||
}
|
||||
conditions.Delete(obj, sourcev1.StorageOperationFailedCondition)
|
||||
return sreconcile.ResultSuccess, nil
|
||||
}
|
||||
|
||||
// reconcileDelete handles the deletion of the object.
|
||||
// It first garbage collects all Artifacts for the object from the Storage.
|
||||
// Removing the finalizer from the object if successful.
|
||||
func (r *OCIRepositoryReconciler) reconcileDelete(ctx context.Context, obj *sourcev1.OCIRepository) (sreconcile.Result, error) {
|
||||
// Garbage collect the resource's artifacts
|
||||
if err := r.garbageCollect(ctx, obj); err != nil {
|
||||
// Return the error so we retry the failed garbage collection
|
||||
return sreconcile.ResultEmpty, err
|
||||
}
|
||||
|
||||
// Remove our finalizer from the list
|
||||
controllerutil.RemoveFinalizer(obj, sourcev1.SourceFinalizer)
|
||||
|
||||
// Stop reconciliation as the object is being deleted
|
||||
return sreconcile.ResultEmpty, nil
|
||||
}
|
||||
|
||||
// garbageCollect performs a garbage collection for the given object.
|
||||
//
|
||||
// It removes all but the current Artifact from the Storage, unless the
|
||||
// deletion timestamp on the object is set. Which will result in the
|
||||
// removal of all Artifacts for the objects.
|
||||
func (r *OCIRepositoryReconciler) garbageCollect(ctx context.Context, obj *sourcev1.OCIRepository) error {
|
||||
if !obj.DeletionTimestamp.IsZero() {
|
||||
if deleted, err := r.Storage.RemoveAll(r.Storage.NewArtifactFor(obj.Kind, obj.GetObjectMeta(), "", "*")); err != nil {
|
||||
return serror.NewGeneric(
|
||||
fmt.Errorf("garbage collection for deleted resource failed: %w", err),
|
||||
"GarbageCollectionFailed",
|
||||
)
|
||||
} else if deleted != "" {
|
||||
r.eventLogf(ctx, obj, events.EventTypeTrace, "GarbageCollectionSucceeded",
|
||||
"garbage collected artifacts for deleted resource")
|
||||
}
|
||||
obj.Status.Artifact = nil
|
||||
return nil
|
||||
}
|
||||
if obj.GetArtifact() != nil {
|
||||
delFiles, err := r.Storage.GarbageCollect(ctx, *obj.GetArtifact(), time.Second*5)
|
||||
if err != nil {
|
||||
return serror.NewGeneric(
|
||||
fmt.Errorf("garbage collection of artifacts failed: %w", err),
|
||||
"GarbageCollectionFailed",
|
||||
)
|
||||
}
|
||||
if len(delFiles) > 0 {
|
||||
r.eventLogf(ctx, obj, events.EventTypeTrace, "GarbageCollectionSucceeded",
|
||||
fmt.Sprintf("garbage collected %d artifacts", len(delFiles)))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// eventLogf records events, and logs at the same time.
|
||||
//
|
||||
// This log is different from the debug log in the EventRecorder, in the sense
|
||||
// that this is a simple log. While the debug log contains complete details
|
||||
// about the event.
|
||||
func (r *OCIRepositoryReconciler) eventLogf(ctx context.Context,
|
||||
obj runtime.Object, eventType string, reason string, messageFmt string, args ...interface{}) {
|
||||
msg := fmt.Sprintf(messageFmt, args...)
|
||||
// Log and emit event.
|
||||
if eventType == corev1.EventTypeWarning {
|
||||
ctrl.LoggerFrom(ctx).Error(errors.New(reason), msg)
|
||||
} else {
|
||||
ctrl.LoggerFrom(ctx).Info(msg)
|
||||
}
|
||||
r.Eventf(obj, eventType, reason, msg)
|
||||
}
|
||||
|
||||
// notify emits notification related to the reconciliation.
|
||||
func (r *OCIRepositoryReconciler) notify(ctx context.Context,
|
||||
oldObj, newObj *sourcev1.OCIRepository, res sreconcile.Result, resErr error) {
|
||||
// Notify successful reconciliation for new artifact and recovery from any
|
||||
// failure.
|
||||
if resErr == nil && res == sreconcile.ResultSuccess && newObj.Status.Artifact != nil {
|
||||
annotations := map[string]string{
|
||||
sourcev1.GroupVersion.Group + "/revision": newObj.Status.Artifact.Revision,
|
||||
sourcev1.GroupVersion.Group + "/checksum": newObj.Status.Artifact.Checksum,
|
||||
}
|
||||
|
||||
var oldChecksum string
|
||||
if oldObj.GetArtifact() != nil {
|
||||
oldChecksum = oldObj.GetArtifact().Checksum
|
||||
}
|
||||
|
||||
message := fmt.Sprintf("stored artifact with digest '%s' from '%s'", newObj.Status.Artifact.Revision, newObj.Spec.URL)
|
||||
|
||||
// enrich message with upstream annotations if found
|
||||
if info := newObj.GetArtifact().Metadata; info != nil {
|
||||
var source, revision string
|
||||
if val, ok := info[oci.SourceAnnotation]; ok {
|
||||
source = val
|
||||
}
|
||||
if val, ok := info[oci.RevisionAnnotation]; ok {
|
||||
revision = val
|
||||
}
|
||||
if source != "" && revision != "" {
|
||||
message = fmt.Sprintf("%s, origin source '%s', origin revision '%s'", message, source, revision)
|
||||
}
|
||||
}
|
||||
|
||||
// Notify on new artifact and failure recovery.
|
||||
if oldChecksum != newObj.GetArtifact().Checksum {
|
||||
r.AnnotatedEventf(newObj, annotations, corev1.EventTypeNormal,
|
||||
"NewArtifact", message)
|
||||
ctrl.LoggerFrom(ctx).Info(message)
|
||||
} else {
|
||||
if sreconcile.FailureRecovery(oldObj, newObj, ociRepositoryFailConditions) {
|
||||
r.AnnotatedEventf(newObj, annotations, corev1.EventTypeNormal,
|
||||
meta.SucceededReason, message)
|
||||
ctrl.LoggerFrom(ctx).Info(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -117,41 +117,33 @@ type registryClientTestServer struct {
|
|||
registryClient *helmreg.Client
|
||||
}
|
||||
|
||||
func setupRegistryServer(ctx context.Context) (*registryClientTestServer, error) {
|
||||
type registryOptions struct {
|
||||
withBasicAuth bool
|
||||
withTLS bool
|
||||
}
|
||||
|
||||
func setupRegistryServer(ctx context.Context, workspaceDir string, opts registryOptions) (*registryClientTestServer, error) {
|
||||
server := ®istryClientTestServer{}
|
||||
|
||||
// Create a temporary workspace directory for the registry
|
||||
workspaceDir, err := os.MkdirTemp("", "registry-test-")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create workspace directory: %w", err)
|
||||
if workspaceDir == "" {
|
||||
return nil, fmt.Errorf("workspace directory cannot be an empty string")
|
||||
}
|
||||
|
||||
server.workspaceDir = workspaceDir
|
||||
|
||||
var out bytes.Buffer
|
||||
server.out = &out
|
||||
|
||||
// init test client
|
||||
server.registryClient, err = helmreg.NewClient(
|
||||
client, err := helmreg.NewClient(
|
||||
helmreg.ClientOptDebug(true),
|
||||
helmreg.ClientOptWriter(server.out),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create registry client: %s", err)
|
||||
}
|
||||
server.registryClient = client
|
||||
|
||||
// create htpasswd file (w BCrypt, which is required)
|
||||
pwBytes, err := bcrypt.GenerateFromPassword([]byte(testRegistryPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate password: %s", err)
|
||||
}
|
||||
|
||||
htpasswdPath := filepath.Join(workspaceDir, testRegistryHtpasswdFileBasename)
|
||||
err = ioutil.WriteFile(htpasswdPath, []byte(fmt.Sprintf("%s:%s\n", testRegistryUsername, string(pwBytes))), 0644)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create htpasswd file: %s", err)
|
||||
}
|
||||
|
||||
// Registry config
|
||||
config := &configuration.Configuration{}
|
||||
port, err := freeport.GetFreePort()
|
||||
if err != nil {
|
||||
|
@ -161,13 +153,37 @@ func setupRegistryServer(ctx context.Context) (*registryClientTestServer, error)
|
|||
server.registryHost = fmt.Sprintf("localhost:%d", port)
|
||||
config.HTTP.Addr = fmt.Sprintf("127.0.0.1:%d", port)
|
||||
config.HTTP.DrainTimeout = time.Duration(10) * time.Second
|
||||
config.Log.AccessLog.Disabled = true
|
||||
config.Log.Level = "error"
|
||||
config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}}
|
||||
config.Auth = configuration.Auth{
|
||||
"htpasswd": configuration.Parameters{
|
||||
"realm": "localhost",
|
||||
"path": htpasswdPath,
|
||||
},
|
||||
|
||||
if opts.withBasicAuth {
|
||||
// create htpasswd file (w BCrypt, which is required)
|
||||
pwBytes, err := bcrypt.GenerateFromPassword([]byte(testRegistryPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate password: %s", err)
|
||||
}
|
||||
|
||||
htpasswdPath := filepath.Join(workspaceDir, testRegistryHtpasswdFileBasename)
|
||||
err = ioutil.WriteFile(htpasswdPath, []byte(fmt.Sprintf("%s:%s\n", testRegistryUsername, string(pwBytes))), 0644)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create htpasswd file: %s", err)
|
||||
}
|
||||
|
||||
// Registry config
|
||||
config.Auth = configuration.Auth{
|
||||
"htpasswd": configuration.Parameters{
|
||||
"realm": "localhost",
|
||||
"path": htpasswdPath,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if opts.withTLS {
|
||||
config.HTTP.TLS.Certificate = "testdata/certs/server.pem"
|
||||
config.HTTP.TLS.Key = "testdata/certs/server-key.pem"
|
||||
}
|
||||
|
||||
dockerRegistry, err := dockerRegistry.NewRegistry(ctx, config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create docker registry: %w", err)
|
||||
|
@ -203,7 +219,13 @@ func TestMain(m *testing.M) {
|
|||
|
||||
testMetricsH = controller.MustMakeMetrics(testEnv)
|
||||
|
||||
testRegistryServer, err = setupRegistryServer(ctx)
|
||||
testWorkspaceDir, err := os.MkdirTemp("", "registry-test-")
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to create workspace directory: %v", err))
|
||||
}
|
||||
testRegistryServer, err = setupRegistryServer(ctx, testWorkspaceDir, registryOptions{
|
||||
withBasicAuth: true,
|
||||
})
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to create a test registry server: %v", err))
|
||||
}
|
||||
|
@ -235,6 +257,15 @@ func TestMain(m *testing.M) {
|
|||
testCache = cache.New(5, 1*time.Second)
|
||||
cacheRecorder := cache.MustMakeMetrics()
|
||||
|
||||
if err := (&OCIRepositoryReconciler{
|
||||
Client: testEnv,
|
||||
EventRecorder: record.NewFakeRecorder(32),
|
||||
Metrics: testMetricsH,
|
||||
Storage: testStorage,
|
||||
}).SetupWithManager(testEnv); err != nil {
|
||||
panic(fmt.Sprintf("Failed to start OCIRepositoryReconciler: %v", err))
|
||||
}
|
||||
|
||||
if err := (&HelmRepositoryReconciler{
|
||||
Client: testEnv,
|
||||
EventRecorder: record.NewFakeRecorder(32),
|
||||
|
@ -292,7 +323,7 @@ func TestMain(m *testing.M) {
|
|||
panic(fmt.Sprintf("Failed to remove storage server dir: %v", err))
|
||||
}
|
||||
|
||||
if err := os.RemoveAll(testRegistryServer.workspaceDir); err != nil {
|
||||
if err := os.RemoveAll(testWorkspaceDir); err != nil {
|
||||
panic(fmt.Sprintf("Failed to remove registry workspace dir: %v", err))
|
||||
}
|
||||
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -16,6 +16,8 @@ Resource Types:
|
|||
<a href="#source.toolkit.fluxcd.io/v1beta2.HelmChart">HelmChart</a>
|
||||
</li><li>
|
||||
<a href="#source.toolkit.fluxcd.io/v1beta2.HelmRepository">HelmRepository</a>
|
||||
</li><li>
|
||||
<a href="#source.toolkit.fluxcd.io/v1beta2.OCIRepository">OCIRepository</a>
|
||||
</li></ul>
|
||||
<h3 id="source.toolkit.fluxcd.io/v1beta2.Bucket">Bucket
|
||||
</h3>
|
||||
|
@ -880,6 +882,229 @@ HelmRepositoryStatus
|
|||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<h3 id="source.toolkit.fluxcd.io/v1beta2.OCIRepository">OCIRepository
|
||||
</h3>
|
||||
<p>OCIRepository is the Schema for the ocirepositories API</p>
|
||||
<div class="md-typeset__scrollwrap">
|
||||
<div class="md-typeset__table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<code>apiVersion</code><br>
|
||||
string</td>
|
||||
<td>
|
||||
<code>source.toolkit.fluxcd.io/v1beta2</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>kind</code><br>
|
||||
string
|
||||
</td>
|
||||
<td>
|
||||
<code>OCIRepository</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>metadata</code><br>
|
||||
<em>
|
||||
<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#objectmeta-v1-meta">
|
||||
Kubernetes meta/v1.ObjectMeta
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
Refer to the Kubernetes API documentation for the fields of the
|
||||
<code>metadata</code> field.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>spec</code><br>
|
||||
<em>
|
||||
<a href="#source.toolkit.fluxcd.io/v1beta2.OCIRepositorySpec">
|
||||
OCIRepositorySpec
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<br/>
|
||||
<br/>
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<code>url</code><br>
|
||||
<em>
|
||||
string
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<p>URL is a reference to an OCI artifact repository hosted
|
||||
on a remote container registry.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>ref</code><br>
|
||||
<em>
|
||||
<a href="#source.toolkit.fluxcd.io/v1beta2.OCIRepositoryRef">
|
||||
OCIRepositoryRef
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>The OCI reference to pull and monitor for changes,
|
||||
defaults to the latest tag.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>provider</code><br>
|
||||
<em>
|
||||
string
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>The provider used for authentication, can be ‘aws’, ‘azure’, ‘gcp’ or ‘generic’.
|
||||
When not specified, defaults to ‘generic’.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>secretRef</code><br>
|
||||
<em>
|
||||
<a href="https://godoc.org/github.com/fluxcd/pkg/apis/meta#LocalObjectReference">
|
||||
github.com/fluxcd/pkg/apis/meta.LocalObjectReference
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>SecretRef contains the secret name containing the registry login
|
||||
credentials to resolve image metadata.
|
||||
The secret must be of type kubernetes.io/dockerconfigjson.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>serviceAccountName</code><br>
|
||||
<em>
|
||||
string
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>ServiceAccountName is the name of the Kubernetes ServiceAccount used to authenticate
|
||||
the image pull if the service account has attached pull secrets. For more information:
|
||||
<a href="https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#add-imagepullsecrets-to-a-service-account">https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#add-imagepullsecrets-to-a-service-account</a></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>certSecretRef</code><br>
|
||||
<em>
|
||||
<a href="https://godoc.org/github.com/fluxcd/pkg/apis/meta#LocalObjectReference">
|
||||
github.com/fluxcd/pkg/apis/meta.LocalObjectReference
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>CertSecretRef can be given the name of a secret containing
|
||||
either or both of</p>
|
||||
<ul>
|
||||
<li>a PEM-encoded client certificate (<code>certFile</code>) and private
|
||||
key (<code>keyFile</code>);</li>
|
||||
<li>a PEM-encoded CA certificate (<code>caFile</code>)</li>
|
||||
</ul>
|
||||
<p>and whichever are supplied, will be used for connecting to the
|
||||
registry. The client cert and key are useful if you are
|
||||
authenticating with a certificate; the CA cert is useful if
|
||||
you are using a self-signed server certificate.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>interval</code><br>
|
||||
<em>
|
||||
<a href="https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#Duration">
|
||||
Kubernetes meta/v1.Duration
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<p>The interval at which to check for image updates.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>timeout</code><br>
|
||||
<em>
|
||||
<a href="https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#Duration">
|
||||
Kubernetes meta/v1.Duration
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>The timeout for remote OCI Repository operations like pulling, defaults to 60s.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>ignore</code><br>
|
||||
<em>
|
||||
string
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>Ignore overrides the set of excluded patterns in the .sourceignore format
|
||||
(which is the same as .gitignore). If not provided, a default will be used,
|
||||
consult the documentation for your version to find out what those are.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>suspend</code><br>
|
||||
<em>
|
||||
bool
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>This flag tells the controller to suspend the reconciliation of this source.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>status</code><br>
|
||||
<em>
|
||||
<a href="#source.toolkit.fluxcd.io/v1beta2.OCIRepositoryStatus">
|
||||
OCIRepositoryStatus
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<h3 id="source.toolkit.fluxcd.io/v1beta2.Artifact">Artifact
|
||||
</h3>
|
||||
<p>
|
||||
|
@ -887,7 +1112,8 @@ HelmRepositoryStatus
|
|||
<a href="#source.toolkit.fluxcd.io/v1beta2.BucketStatus">BucketStatus</a>,
|
||||
<a href="#source.toolkit.fluxcd.io/v1beta2.GitRepositoryStatus">GitRepositoryStatus</a>,
|
||||
<a href="#source.toolkit.fluxcd.io/v1beta2.HelmChartStatus">HelmChartStatus</a>,
|
||||
<a href="#source.toolkit.fluxcd.io/v1beta2.HelmRepositoryStatus">HelmRepositoryStatus</a>)
|
||||
<a href="#source.toolkit.fluxcd.io/v1beta2.HelmRepositoryStatus">HelmRepositoryStatus</a>,
|
||||
<a href="#source.toolkit.fluxcd.io/v1beta2.OCIRepositoryStatus">OCIRepositoryStatus</a>)
|
||||
</p>
|
||||
<p>Artifact represents the output of a Source reconciliation.</p>
|
||||
<div class="md-typeset__scrollwrap">
|
||||
|
@ -977,6 +1203,18 @@ int64
|
|||
<p>Size is the number of bytes in the file.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>metadata</code><br>
|
||||
<em>
|
||||
map[string]string
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>Metadata holds upstream information such as OCI annotations.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
@ -2291,6 +2529,363 @@ string
|
|||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<h3 id="source.toolkit.fluxcd.io/v1beta2.OCIRepositoryRef">OCIRepositoryRef
|
||||
</h3>
|
||||
<p>
|
||||
(<em>Appears on:</em>
|
||||
<a href="#source.toolkit.fluxcd.io/v1beta2.OCIRepositorySpec">OCIRepositorySpec</a>)
|
||||
</p>
|
||||
<p>OCIRepositoryRef defines the image reference for the OCIRepository’s URL</p>
|
||||
<div class="md-typeset__scrollwrap">
|
||||
<div class="md-typeset__table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<code>digest</code><br>
|
||||
<em>
|
||||
string
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>Digest is the image digest to pull, takes precedence over SemVer.
|
||||
The value should be in the format ‘sha256:<HASH>’.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>semver</code><br>
|
||||
<em>
|
||||
string
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>SemVer is the range of tags to pull selecting the latest within
|
||||
the range, takes precedence over Tag.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>tag</code><br>
|
||||
<em>
|
||||
string
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>Tag is the image tag to pull, defaults to latest.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<h3 id="source.toolkit.fluxcd.io/v1beta2.OCIRepositorySpec">OCIRepositorySpec
|
||||
</h3>
|
||||
<p>
|
||||
(<em>Appears on:</em>
|
||||
<a href="#source.toolkit.fluxcd.io/v1beta2.OCIRepository">OCIRepository</a>)
|
||||
</p>
|
||||
<p>OCIRepositorySpec defines the desired state of OCIRepository</p>
|
||||
<div class="md-typeset__scrollwrap">
|
||||
<div class="md-typeset__table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<code>url</code><br>
|
||||
<em>
|
||||
string
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<p>URL is a reference to an OCI artifact repository hosted
|
||||
on a remote container registry.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>ref</code><br>
|
||||
<em>
|
||||
<a href="#source.toolkit.fluxcd.io/v1beta2.OCIRepositoryRef">
|
||||
OCIRepositoryRef
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>The OCI reference to pull and monitor for changes,
|
||||
defaults to the latest tag.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>provider</code><br>
|
||||
<em>
|
||||
string
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>The provider used for authentication, can be ‘aws’, ‘azure’, ‘gcp’ or ‘generic’.
|
||||
When not specified, defaults to ‘generic’.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>secretRef</code><br>
|
||||
<em>
|
||||
<a href="https://godoc.org/github.com/fluxcd/pkg/apis/meta#LocalObjectReference">
|
||||
github.com/fluxcd/pkg/apis/meta.LocalObjectReference
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>SecretRef contains the secret name containing the registry login
|
||||
credentials to resolve image metadata.
|
||||
The secret must be of type kubernetes.io/dockerconfigjson.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>serviceAccountName</code><br>
|
||||
<em>
|
||||
string
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>ServiceAccountName is the name of the Kubernetes ServiceAccount used to authenticate
|
||||
the image pull if the service account has attached pull secrets. For more information:
|
||||
<a href="https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#add-imagepullsecrets-to-a-service-account">https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#add-imagepullsecrets-to-a-service-account</a></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>certSecretRef</code><br>
|
||||
<em>
|
||||
<a href="https://godoc.org/github.com/fluxcd/pkg/apis/meta#LocalObjectReference">
|
||||
github.com/fluxcd/pkg/apis/meta.LocalObjectReference
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>CertSecretRef can be given the name of a secret containing
|
||||
either or both of</p>
|
||||
<ul>
|
||||
<li>a PEM-encoded client certificate (<code>certFile</code>) and private
|
||||
key (<code>keyFile</code>);</li>
|
||||
<li>a PEM-encoded CA certificate (<code>caFile</code>)</li>
|
||||
</ul>
|
||||
<p>and whichever are supplied, will be used for connecting to the
|
||||
registry. The client cert and key are useful if you are
|
||||
authenticating with a certificate; the CA cert is useful if
|
||||
you are using a self-signed server certificate.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>interval</code><br>
|
||||
<em>
|
||||
<a href="https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#Duration">
|
||||
Kubernetes meta/v1.Duration
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<p>The interval at which to check for image updates.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>timeout</code><br>
|
||||
<em>
|
||||
<a href="https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#Duration">
|
||||
Kubernetes meta/v1.Duration
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>The timeout for remote OCI Repository operations like pulling, defaults to 60s.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>ignore</code><br>
|
||||
<em>
|
||||
string
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>Ignore overrides the set of excluded patterns in the .sourceignore format
|
||||
(which is the same as .gitignore). If not provided, a default will be used,
|
||||
consult the documentation for your version to find out what those are.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>suspend</code><br>
|
||||
<em>
|
||||
bool
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>This flag tells the controller to suspend the reconciliation of this source.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<h3 id="source.toolkit.fluxcd.io/v1beta2.OCIRepositoryStatus">OCIRepositoryStatus
|
||||
</h3>
|
||||
<p>
|
||||
(<em>Appears on:</em>
|
||||
<a href="#source.toolkit.fluxcd.io/v1beta2.OCIRepository">OCIRepository</a>)
|
||||
</p>
|
||||
<p>OCIRepositoryStatus defines the observed state of OCIRepository</p>
|
||||
<div class="md-typeset__scrollwrap">
|
||||
<div class="md-typeset__table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<code>observedGeneration</code><br>
|
||||
<em>
|
||||
int64
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>ObservedGeneration is the last observed generation.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>conditions</code><br>
|
||||
<em>
|
||||
<a href="https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#Condition">
|
||||
[]Kubernetes meta/v1.Condition
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>Conditions holds the conditions for the OCIRepository.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>url</code><br>
|
||||
<em>
|
||||
string
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>URL is the download link for the artifact output of the last OCI Repository sync.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>artifact</code><br>
|
||||
<em>
|
||||
<a href="#source.toolkit.fluxcd.io/v1beta2.Artifact">
|
||||
Artifact
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>Artifact represents the output of the last successful OCI Repository sync.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>ReconcileRequestStatus</code><br>
|
||||
<em>
|
||||
<a href="https://godoc.org/github.com/fluxcd/pkg/apis/meta#ReconcileRequestStatus">
|
||||
github.com/fluxcd/pkg/apis/meta.ReconcileRequestStatus
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<p>
|
||||
(Members of <code>ReconcileRequestStatus</code> are embedded into this type.)
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<h3 id="source.toolkit.fluxcd.io/v1beta2.OCIRepositoryVerification">OCIRepositoryVerification
|
||||
</h3>
|
||||
<p>OCIRepositoryVerification verifies the authenticity of an OCI Artifact</p>
|
||||
<div class="md-typeset__scrollwrap">
|
||||
<div class="md-typeset__table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<code>provider</code><br>
|
||||
<em>
|
||||
string
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<p>Provider specifies the technology used to sign the OCI Artifact.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>secretRef</code><br>
|
||||
<em>
|
||||
<a href="https://godoc.org/github.com/fluxcd/pkg/apis/meta#LocalObjectReference">
|
||||
github.com/fluxcd/pkg/apis/meta.LocalObjectReference
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<p>SecretRef specifies the Kubernetes Secret containing the
|
||||
trusted public keys.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<h3 id="source.toolkit.fluxcd.io/v1beta2.Source">Source
|
||||
</h3>
|
||||
<p>Source interface must be supported by all API types.
|
||||
|
|
|
@ -6,6 +6,7 @@ This is the v1beta2 API specification for defining the desired state sources of
|
|||
|
||||
* Source kinds:
|
||||
+ [GitRepository](gitrepositories.md)
|
||||
+ [OCIRepository](ocirepositories.md)
|
||||
+ [HelmRepository](helmrepositories.md)
|
||||
+ [HelmChart](helmcharts.md)
|
||||
+ [Bucket](buckets.md)
|
||||
|
|
|
@ -0,0 +1,673 @@
|
|||
# OCI Repositories
|
||||
|
||||
The `OCIRepository` API defines a Source to produce an Artifact for an OCI
|
||||
repository.
|
||||
|
||||
## Example
|
||||
|
||||
The following is an example of an OCIRepository. It creates a tarball
|
||||
(`.tar.gz`) Artifact with the fetched data from an OCI repository for the
|
||||
resolved digest.
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: source.toolkit.fluxcd.io/v1beta2
|
||||
kind: OCIRepository
|
||||
metadata:
|
||||
name: podinfo
|
||||
namespace: default
|
||||
spec:
|
||||
interval: 5m0s
|
||||
url: oci://ghcr.io/stefanprodan/manifests/podinfo
|
||||
ref:
|
||||
tag: latest
|
||||
```
|
||||
|
||||
In the above example:
|
||||
|
||||
- An OCIRepository named `podinfo` is created, indicated by the
|
||||
`.metadata.name` field.
|
||||
- The source-controller checks the OCI repository every five minutes, indicated
|
||||
by the `.spec.interval` field.
|
||||
- It pulls the `latest` tag of the `ghcr.io/stefanprodan/manifests/podinfo`
|
||||
repository, indicated by the `.spec.ref.tag` and `.spec.url` fields.
|
||||
- The resolved SHA256 digest is used as the Artifact
|
||||
revision, reported in-cluster in the `.status.artifact.revision` field.
|
||||
- When the current OCIRepository digest differs from the latest fetched
|
||||
digest, a new Artifact is archived.
|
||||
- The new Artifact is reported in the `.status.artifact` field.
|
||||
|
||||
You can run this example by saving the manifest into `ocirepository.yaml`.
|
||||
|
||||
1. Apply the resource on the cluster:
|
||||
|
||||
```sh
|
||||
kubectl apply -f ocirepository.yaml
|
||||
```
|
||||
|
||||
2. Run `kubectl get ocirepository` to see the OCIRepository:
|
||||
|
||||
```console
|
||||
NAME URL AGE READY STATUS
|
||||
podinfo oci://ghcr.io/stefanprodan/manifests/podinfo 5s True stored artifact with digest '3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de'
|
||||
```
|
||||
|
||||
3. Run `kubectl describe ocirepository podinfo` to see the [Artifact](#artifact)
|
||||
and [Conditions](#conditions) in the OCIRepository's Status:
|
||||
|
||||
```console
|
||||
...
|
||||
Status:
|
||||
Artifact:
|
||||
Checksum: d7e924b4882e55b97627355c7b3d2e711e9b54303afa2f50c25377f4df66a83b
|
||||
Last Update Time: 2022-06-14T11:23:36Z
|
||||
Path: ocirepository/default/podinfo/3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de.tar.gz
|
||||
Revision: 3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de
|
||||
URL: http://source-controller.flux-system.svc.cluster.local./ocirepository/oci/podinfo/3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de.tar.gz
|
||||
Conditions:
|
||||
Last Transition Time: 2022-06-14T11:23:36Z
|
||||
Message: stored artifact for digest '3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de'
|
||||
Observed Generation: 1
|
||||
Reason: Succeeded
|
||||
Status: True
|
||||
Type: Ready
|
||||
Last Transition Time: 2022-06-14T11:23:36Z
|
||||
Message: stored artifact for digest '3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de'
|
||||
Observed Generation: 1
|
||||
Reason: Succeeded
|
||||
Status: True
|
||||
Type: ArtifactInStorage
|
||||
Observed Generation: 1
|
||||
URL: http://source-controller.source-system.svc.cluster.local./gitrepository/default/podinfo/latest.tar.gz
|
||||
Events:
|
||||
Type Reason Age From Message
|
||||
---- ------ ---- ---- -------
|
||||
Normal NewArtifact 62s source-controller stored artifact with digest '3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de' from 'oci://ghcr.io/stefanprodan/manifests/podinfo'
|
||||
```
|
||||
|
||||
## Writing an OCIRepository spec
|
||||
|
||||
As with all other Kubernetes config, an OCIRepository needs `apiVersion`,
|
||||
`kind`, and `metadata` fields. The name of an OCIRepository object must be a
|
||||
valid [DNS subdomain name](https://kubernetes.io/docs/concepts/overview/working-with-objects/names#dns-subdomain-names).
|
||||
|
||||
An OCIRepository also needs a
|
||||
[`.spec` section](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#spec-and-status).
|
||||
|
||||
### URL
|
||||
|
||||
`.spec.url` is a required field that specifies the address of the
|
||||
container image repository in the format `oci://<host>:<port>/<org-name>/<repo-name>`.
|
||||
|
||||
**Note:** that specifying a tag or digest is not acceptable for this field.
|
||||
|
||||
### Provider
|
||||
|
||||
`.spec.provider` is an optional field that allows specifying an OIDC provider used for
|
||||
authentication purposes.
|
||||
|
||||
Supported options are:
|
||||
|
||||
- `generic`
|
||||
- `aws`
|
||||
- `azure`
|
||||
- `gcp`
|
||||
|
||||
The `generic` provider can be used for public repositories or when
|
||||
static credentials are used for authentication, either with
|
||||
`spec.secretRef` or `spec.serviceAccountName`.
|
||||
If you do not specify `.spec.provider`, it defaults to `generic`.
|
||||
|
||||
The `aws` provider can be used when the source-controller service account
|
||||
is associated with an AWS IAM Role using IRSA that grants read-only access to ECR.
|
||||
|
||||
The `azure` provider can be used when the source-controller pods are associated
|
||||
with an Azure AAD Pod Identity that grants read-only access to ACR.
|
||||
|
||||
The `gcp` provider can be used when the source-controller service account
|
||||
is associated with a GCP IAM Role using Workload Identity that grants
|
||||
read-only access to Artifact Registry.
|
||||
|
||||
### Secret reference
|
||||
|
||||
`.spec.secretRef.name` is an optional field to specify a name reference to a
|
||||
Secret in the same namespace as the OCIRepository, containing authentication
|
||||
credentials for the OCI repository.
|
||||
|
||||
This secret is expected to be in the same format as [`imagePullSecrets`][image-pull-secrets].
|
||||
The usual way to create such a secret is with:
|
||||
|
||||
```sh
|
||||
kubectl create secret docker-registry ...
|
||||
```
|
||||
|
||||
### Service Account reference
|
||||
|
||||
`.spec.serviceAccountName` is an optional field to specify a name reference to a
|
||||
Service Account in the same namespace as the OCIRepository. The controller will
|
||||
fetch the image pull secrets attached to the service account and use them for authentication.
|
||||
|
||||
**Note:** that for a publicly accessible image repository, you don't need to provide a `secretRef`
|
||||
nor `serviceAccountName`.
|
||||
|
||||
### TLS Certificates
|
||||
|
||||
`.spec.certSecretRef` field names a secret with TLS certificate data. This is for two separate
|
||||
purposes:
|
||||
|
||||
- to provide a client certificate and private key, if you use a certificate to authenticate with
|
||||
the container registry; and,
|
||||
- to provide a CA certificate, if the registry uses a self-signed certificate.
|
||||
|
||||
These will often go together, if you are hosting a container registry yourself. All the files in the
|
||||
secret are expected to be [PEM-encoded][pem-encoding]. This is an ASCII format for certificates and
|
||||
keys; `openssl` and such tools will typically give you an option of PEM output.
|
||||
|
||||
Assuming you have obtained a certificate file and private key and put them in the files `client.crt`
|
||||
and `client.key` respectively, you can create a secret with `kubectl` like this:
|
||||
|
||||
```bash
|
||||
kubectl create secret generic tls-certs \
|
||||
--from-file=certFile=client.crt \
|
||||
--from-file=keyFile=client.key
|
||||
```
|
||||
|
||||
You could also [prepare a secret and encrypt it][sops-guide]; the important bit is that the data
|
||||
keys in the secret are `certFile` and `keyFile`.
|
||||
|
||||
If you have a CA certificate for the client to use, the data key for that is `caFile`. Adapting the
|
||||
previous example, if you have the certificate in the file `ca.crt`, and the client certificate and
|
||||
key as before, the whole command would be:
|
||||
|
||||
```bash
|
||||
kubectl create secret generic tls-certs \
|
||||
--from-file=certFile=client.crt \
|
||||
--from-file=keyFile=client.key \
|
||||
--from-file=caFile=ca.crt
|
||||
```
|
||||
|
||||
### Interval
|
||||
|
||||
`.spec.interval` is a required field that specifies the interval at which the
|
||||
OCI repository must be fetched.
|
||||
|
||||
After successfully reconciling the object, the source-controller requeues it
|
||||
for inspection after the specified interval. The value must be in a
|
||||
[Go recognized duration string format](https://pkg.go.dev/time#ParseDuration),
|
||||
e.g. `10m0s` to reconcile the object every 10 minutes.
|
||||
|
||||
If the `.metadata.generation` of a resource changes (due to e.g. a change to
|
||||
the spec), this is handled instantly outside the interval window.
|
||||
|
||||
### Timeout
|
||||
|
||||
`.spec.timeout` is an optional field to specify a timeout for OCI operations
|
||||
like pulling. The value must be in a
|
||||
[Go recognized duration string format](https://pkg.go.dev/time#ParseDuration),
|
||||
e.g. `1m30s` for a timeout of one minute and thirty seconds. The default value
|
||||
is `60s`.
|
||||
|
||||
### Reference
|
||||
|
||||
`.spec.ref` is an optional field to specify the OCI reference to resolve and
|
||||
watch for changes. References are specified in one or more subfields
|
||||
(`.tag`, `.semver`, `.digest`), with latter listed fields taking
|
||||
precedence over earlier ones. If not specified, it defaults to the `latest`
|
||||
tag.
|
||||
|
||||
#### Tag example
|
||||
|
||||
To pull a specific tag, use `.spec.ref.tag`:
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: source.toolkit.fluxcd.io/v1beta2
|
||||
kind: OCIRepository
|
||||
metadata:
|
||||
name: <repository-name>
|
||||
spec:
|
||||
ref:
|
||||
tag: "<tag-name>"
|
||||
```
|
||||
|
||||
#### SemVer example
|
||||
|
||||
To pull a tag based on a
|
||||
[SemVer range](https://github.com/Masterminds/semver#checking-version-constraints),
|
||||
use `.spec.ref.semver`:
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: source.toolkit.fluxcd.io/v1beta2
|
||||
kind: OCIRepository
|
||||
metadata:
|
||||
name: <repository-name>
|
||||
spec:
|
||||
ref:
|
||||
# SemVer range reference: https://github.com/Masterminds/semver#checking-version-constraints
|
||||
semver: "<semver-range>"
|
||||
```
|
||||
|
||||
This field takes precedence over [`.tag`](#tag-example).
|
||||
|
||||
#### Digest example
|
||||
|
||||
To pull a specific digest, use `.spec.ref.digest`:
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: source.toolkit.fluxcd.io/v1beta2
|
||||
kind: OCIRepository
|
||||
metadata:
|
||||
name: <repository-name>
|
||||
spec:
|
||||
ref:
|
||||
digest: "sha256:<SHA-value>"
|
||||
```
|
||||
|
||||
This field takes precedence over all other fields.
|
||||
|
||||
### Ignore
|
||||
|
||||
`.spec.ignore` is an optional field to specify rules in [the `.gitignore`
|
||||
pattern format](https://git-scm.com/docs/gitignore#_pattern_format). Paths
|
||||
matching the defined rules are excluded while archiving.
|
||||
|
||||
When specified, `.spec.ignore` overrides the [default exclusion
|
||||
list](#default-exclusions), and may overrule the [`.sourceignore` file
|
||||
exclusions](#sourceignore-file). See [excluding files](#excluding-files)
|
||||
for more information.
|
||||
|
||||
### Suspend
|
||||
|
||||
`.spec.suspend` is an optional field to suspend the reconciliation of a
|
||||
OCIRepository. When set to `true`, the controller will stop reconciling the
|
||||
OCIRepository, and changes to the resource or in the OCI repository will not
|
||||
result in a new Artifact. When the field is set to `false` or removed, it will
|
||||
resume.
|
||||
|
||||
## Working with OCIRepositories
|
||||
|
||||
### Excluding files
|
||||
|
||||
By default, files which match the [default exclusion rules](#default-exclusions)
|
||||
are excluded while archiving the OCI repository contents as an Artifact.
|
||||
It is possible to overwrite and/or overrule the default exclusions using
|
||||
the [`.spec.ignore` field](#ignore).
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: source.toolkit.fluxcd.io/v1beta2
|
||||
kind: OCIRepository
|
||||
metadata:
|
||||
name: <repository-name>
|
||||
spec:
|
||||
ignore: |
|
||||
# exclude all
|
||||
/*
|
||||
# include deploy dir
|
||||
!/deploy
|
||||
# exclude file extensions from deploy dir
|
||||
/deploy/**/*.md
|
||||
/deploy/**/*.txt
|
||||
```
|
||||
|
||||
### Triggering a reconcile
|
||||
|
||||
To manually tell the source-controller to reconcile a OCIRepository outside the
|
||||
[specified interval window](#interval), an OCIRepository can be annotated with
|
||||
`reconcile.fluxcd.io/requestedAt: <arbitrary value>`. Annotating the resource
|
||||
queues the OCIRepository for reconciliation if the `<arbitrary-value>` differs
|
||||
from the last value the controller acted on, as reported in
|
||||
[`.status.lastHandledReconcileAt`](#last-handled-reconcile-at).
|
||||
|
||||
Using `kubectl`:
|
||||
|
||||
```sh
|
||||
kubectl annotate --field-manager=flux-client-side-apply --overwrite ocirepository/<repository-name> reconcile.fluxcd.io/requestedAt="$(date +%s)"
|
||||
```
|
||||
|
||||
Using `flux`:
|
||||
|
||||
```sh
|
||||
flux reconcile source oci <repository-name>
|
||||
```
|
||||
|
||||
### Waiting for `Ready`
|
||||
|
||||
When a change is applied, it is possible to wait for the OCIRepository to reach
|
||||
a [ready state](#ready-gitrepository) using `kubectl`:
|
||||
|
||||
```sh
|
||||
kubectl wait gitrepository/<repository-name> --for=condition=ready --timeout=1m
|
||||
```
|
||||
|
||||
### Suspending and resuming
|
||||
|
||||
When you find yourself in a situation where you temporarily want to pause the
|
||||
reconciliation of an OCIRepository, you can suspend it using the
|
||||
[`.spec.suspend` field](#suspend).
|
||||
|
||||
#### Suspend an OCIRepository
|
||||
|
||||
In your YAML declaration:
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: source.toolkit.fluxcd.io/v1beta2
|
||||
kind: OCIRepository
|
||||
metadata:
|
||||
name: <repository-name>
|
||||
spec:
|
||||
suspend: true
|
||||
```
|
||||
|
||||
Using `kubectl`:
|
||||
|
||||
```sh
|
||||
kubectl patch ocirepository <repository-name> --field-manager=flux-client-side-apply -p '{\"spec\": {\"suspend\" : true }}'
|
||||
```
|
||||
|
||||
Using `flux`:
|
||||
|
||||
```sh
|
||||
flux suspend source oci <repository-name>
|
||||
```
|
||||
|
||||
**Note:** When an OCIRepository has an Artifact and it is suspended, and this
|
||||
Artifact later disappears from the storage due to e.g. the source-controller
|
||||
Pod being evicted from a Node, this will not be reflected in the
|
||||
OCIRepository's Status until it is resumed.
|
||||
|
||||
#### Resume an OCIRepository
|
||||
|
||||
In your YAML declaration, comment out (or remove) the field:
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: source.toolkit.fluxcd.io/v1beta2
|
||||
kind: OCIRepository
|
||||
metadata:
|
||||
name: <repository-name>
|
||||
spec:
|
||||
# suspend: true
|
||||
```
|
||||
|
||||
**Note:** Setting the field value to `false` has the same effect as removing
|
||||
it, but does not allow for "hot patching" using e.g. `kubectl` while practicing
|
||||
GitOps; as the manually applied patch would be overwritten by the declared
|
||||
state in Git.
|
||||
|
||||
Using `kubectl`:
|
||||
|
||||
```sh
|
||||
kubectl patch ocirepository <repository-name> --field-manager=flux-client-side-apply -p '{\"spec\" : {\"suspend\" : false }}'
|
||||
```
|
||||
|
||||
Using `flux`:
|
||||
|
||||
```sh
|
||||
flux resume source oci <repository-name>
|
||||
```
|
||||
|
||||
### Debugging an OCIRepository
|
||||
|
||||
There are several ways to gather information about a OCIRepository for
|
||||
debugging purposes.
|
||||
|
||||
#### Describe the OCIRepository
|
||||
|
||||
Describing an OCIRepository using
|
||||
`kubectl describe ocirepository <repository-name>`
|
||||
displays the latest recorded information for the resource in the `Status` and
|
||||
`Events` sections:
|
||||
|
||||
```console
|
||||
...
|
||||
Status:
|
||||
...
|
||||
Conditions:
|
||||
Last Transition Time: 2022-02-14T09:40:27Z
|
||||
Message: reconciling new object generation (2)
|
||||
Observed Generation: 2
|
||||
Reason: NewGeneration
|
||||
Status: True
|
||||
Type: Reconciling
|
||||
Last Transition Time: 2022-02-14T09:40:27Z
|
||||
Message: failed to pull artifact from 'oci://ghcr.io/stefanprodan/manifests/podinfo': couldn't find tag "0.0.1"
|
||||
Observed Generation: 2
|
||||
Reason: OCIOperationFailed
|
||||
Status: False
|
||||
Type: Ready
|
||||
Last Transition Time: 2022-02-14T09:40:27Z
|
||||
Message: failed to pull artifact from 'oci://ghcr.io/stefanprodan/manifests/podinfo': couldn't find tag "0.0.1"
|
||||
Observed Generation: 2
|
||||
Reason: OCIOperationFailed
|
||||
Status: True
|
||||
Type: FetchFailed
|
||||
Observed Generation: 1
|
||||
URL: http://source-controller.source-system.svc.cluster.local./ocirepository/default/podinfo/latest.tar.gz
|
||||
Events:
|
||||
Type Reason Age From Message
|
||||
---- ------ ---- ---- -------
|
||||
Warning OCIOperationFailed 2s (x9 over 4s) source-controller failed to pull artifact from 'oci://ghcr.io/stefanprodan/manifests/podinfo': couldn't find tag "0.0.1"
|
||||
```
|
||||
|
||||
#### Trace emitted Events
|
||||
|
||||
To view events for specific OCIRepository(s), `kubectl get events` can be used
|
||||
in combination with `--field-sector` to list the Events for specific objects.
|
||||
For example, running
|
||||
|
||||
```sh
|
||||
kubectl get events --field-selector involvedObject.kind=OCIRepository,involvedObject.name=<repository-name>
|
||||
```
|
||||
|
||||
lists
|
||||
|
||||
```console
|
||||
LAST SEEN TYPE REASON OBJECT MESSAGE
|
||||
2m14s Normal NewArtifact ocirepository/<repository-name> stored artifact for digest '3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de'
|
||||
36s Normal ArtifactUpToDate ocirepository/<repository-name> artifact up-to-date with remote digest: '3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de'
|
||||
94s Warning OCIOperationFailed ocirepository/<repository-name> failed to pull artifact from 'oci://ghcr.io/stefanprodan/manifests/podinfo': couldn't find tag "0.0.1"
|
||||
```
|
||||
|
||||
Besides being reported in Events, the reconciliation errors are also logged by
|
||||
the controller. The Flux CLI offer commands for filtering the logs for a
|
||||
specific OCIRepository, e.g.
|
||||
`flux logs --level=error --kind=OCIRepository --name=<repository-name>`.
|
||||
|
||||
## OCIRepository Status
|
||||
|
||||
### Artifact
|
||||
|
||||
The OCIRepository reports the latest synchronized state from the OCI repository
|
||||
as an Artifact object in the `.status.artifact` of the resource.
|
||||
|
||||
The `.status.artifact.revision` holds the SHA256 digest of the upstream OCI artifact.
|
||||
|
||||
The `.status.artifact.metadata` holds the upstream OCI artifact metadata such as the
|
||||
[OpenContainers standard annotations](https://github.com/opencontainers/image-spec/blob/main/annotations.md).
|
||||
If the OCI artifact was created with `flux push artifact`, then the `metadata` will contain the following
|
||||
annotations:
|
||||
- `org.opencontainers.image.created` the date and time on which the artifact was built
|
||||
- `org.opencontainers.image.source` the URL of the Git repository containing the source files
|
||||
- `org.opencontainers.image.revision` the Git branch and commit SHA1 of the source files
|
||||
|
||||
The Artifact file is a gzip compressed TAR archive (`<commit sha>.tar.gz`), and
|
||||
can be retrieved in-cluster from the `.status.artifact.url` HTTP address.
|
||||
|
||||
#### Artifact example
|
||||
|
||||
```yaml
|
||||
apiVersion: source.toolkit.fluxcd.io/v1beta2
|
||||
kind: OCIRepository
|
||||
metadata:
|
||||
name: <repository-name>
|
||||
status:
|
||||
artifact:
|
||||
checksum: 9f3bc0f341d4ecf2bab460cc59320a2a9ea292f01d7b96e32740a9abfd341088
|
||||
lastUpdateTime: "2022-08-08T09:35:45Z"
|
||||
metadata:
|
||||
org.opencontainers.image.created: "2022-08-08T12:31:41+03:00"
|
||||
org.opencontainers.image.revision: 6.1.8/b3b00fe35424a45d373bf4c7214178bc36fd7872
|
||||
org.opencontainers.image.source: https://github.com/stefanprodan/podinfo.git
|
||||
path: ocirepository/<namespace>/<repository-name>/<digest>.tar.gz
|
||||
revision: <digest>
|
||||
url: http://source-controller.<namespace>.svc.cluster.local./ocirepository/<namespace>/<repository-name>/<digest>.tar.gz
|
||||
```
|
||||
|
||||
#### Default exclusions
|
||||
|
||||
The following files and extensions are excluded from the Artifact by
|
||||
default:
|
||||
|
||||
- Git files (`.git/, .gitignore, .gitmodules, .gitattributes`)
|
||||
- File extensions (`.jpg, .jpeg, .gif, .png, .wmv, .flv, .tar.gz, .zip`)
|
||||
- CI configs (`.github/, .circleci/, .travis.yml, .gitlab-ci.yml, appveyor.yml, .drone.yml, cloudbuild.yaml, codeship-services.yml, codeship-steps.yml`)
|
||||
- CLI configs (`.goreleaser.yml, .sops.yaml`)
|
||||
- Flux v1 config (`.flux.yaml`)
|
||||
|
||||
To define your own exclusion rules, see [excluding files](#excluding-files).
|
||||
|
||||
### Conditions
|
||||
|
||||
OCIRepository has various states during its lifecycle, reflected as
|
||||
[Kubernetes Conditions][typical-status-properties].
|
||||
It can be [reconciling](#reconciling-ocirepository) while fetching the remote
|
||||
state, it can be [ready](#ready-ocirepository), or it can [fail during
|
||||
reconciliation](#failed-ocirepository).
|
||||
|
||||
The OCIRepository API is compatible with the [kstatus specification][kstatus-spec],
|
||||
and reports `Reconciling` and `Stalled` conditions where applicable to
|
||||
provide better (timeout) support to solutions polling the OCIRepository to
|
||||
become `Ready`.
|
||||
|
||||
#### Reconciling OCIRepository
|
||||
|
||||
The source-controller marks an OCIRepository as _reconciling_ when one of the
|
||||
following is true:
|
||||
|
||||
- There is no current Artifact for the OCIRepository, or the reported Artifact
|
||||
is determined to have disappeared from the storage.
|
||||
- The generation of the OCIRepository is newer than the [Observed
|
||||
Generation](#observed-generation).
|
||||
- The newly resolved Artifact digest differs from the current Artifact.
|
||||
|
||||
When the OCIRepository is "reconciling", the `Ready` Condition status becomes
|
||||
`False`, and the controller adds a Condition with the following attributes to
|
||||
the OCIRepository's `.status.conditions`:
|
||||
|
||||
- `type: Reconciling`
|
||||
- `status: "True"`
|
||||
- `reason: NewGeneration` | `reason: NoArtifact` | `reason: NewRevision`
|
||||
|
||||
If the reconciling state is due to a new revision, an additional Condition is
|
||||
added with the following attributes:
|
||||
|
||||
- `type: ArtifactOutdated`
|
||||
- `status: "True"`
|
||||
- `reason: NewRevision`
|
||||
|
||||
Both Conditions have a ["negative polarity"][typical-status-properties],
|
||||
and are only present on the OCIRepository while their status value is `"True"`.
|
||||
|
||||
#### Ready OCIRepository
|
||||
|
||||
The source-controller marks an OCIRepository as _ready_ when it has the
|
||||
following characteristics:
|
||||
|
||||
- The OCIRepository reports an [Artifact](#artifact).
|
||||
- The reported Artifact exists in the controller's Artifact storage.
|
||||
- The controller was able to communicate with the remote OCI repository using
|
||||
the current spec.
|
||||
- The digest of the reported Artifact is up-to-date with the latest
|
||||
resolved digest of the remote OCI repository.
|
||||
|
||||
When the OCIRepository is "ready", the controller sets a Condition with the
|
||||
following attributes in the OCIRepository's `.status.conditions`:
|
||||
|
||||
- `type: Ready`
|
||||
- `status: "True"`
|
||||
- `reason: Succeeded`
|
||||
|
||||
This `Ready` Condition will retain a status value of `"True"` until the
|
||||
OCIRepository is marked as [reconciling](#reconciling-gitrepository), or e.g. a
|
||||
[transient error](#failed-gitrepository) occurs due to a temporary network issue.
|
||||
|
||||
When the OCIRepository Artifact is archived in the controller's Artifact
|
||||
storage, the controller sets a Condition with the following attributes in the
|
||||
OCIRepository's `.status.conditions`:
|
||||
|
||||
- `type: ArtifactInStorage`
|
||||
- `status: "True"`
|
||||
- `reason: Succeeded`
|
||||
|
||||
This `ArtifactInStorage` Condition will retain a status value of `"True"` until
|
||||
the Artifact in the storage no longer exists.
|
||||
|
||||
#### Failed OCIRepository
|
||||
|
||||
The source-controller may get stuck trying to produce an Artifact for a
|
||||
OCIRepository without completing. This can occur due to some of the following
|
||||
factors:
|
||||
|
||||
- The remote OCI repository [URL](#url) is temporarily unavailable.
|
||||
- The OCI repository does not exist.
|
||||
- The [Secret reference](#secret-reference) contains a reference to a
|
||||
non-existing Secret.
|
||||
- The credentials in the referenced Secret are invalid.
|
||||
- The OCIRepository spec contains a generic misconfiguration.
|
||||
- A storage related failure when storing the artifact.
|
||||
|
||||
When this happens, the controller sets the `Ready` Condition status to `False`,
|
||||
and adds a Condition with the following attributes to the OCIRepository's
|
||||
`.status.conditions`:
|
||||
|
||||
- `type: FetchFailed` | `type: IncludeUnavailable` | `type: StorageOperationFailed`
|
||||
- `status: "True"`
|
||||
- `reason: AuthenticationFailed` | `reason: OCIArtifactPullFailed` | `reason: OCIArtifactLayerOperationFailed`
|
||||
|
||||
This condition has a ["negative polarity"][typical-status-properties],
|
||||
and is only present on the OCIRepository while the status value is `"True"`.
|
||||
There may be more arbitrary values for the `reason` field to provide accurate
|
||||
reason for a condition.
|
||||
|
||||
While the OCIRepository has one or more of these Conditions, the controller
|
||||
will continue to attempt to produce an Artifact for the resource with an
|
||||
exponential backoff, until it succeeds and the OCIRepository is marked as
|
||||
[ready](#ready-ocirepository).
|
||||
|
||||
Note that a OCIRepository can be [reconciling](#reconciling-ocirepository)
|
||||
while failing at the same time, for example due to a newly introduced
|
||||
configuration issue in the OCIRepository spec.
|
||||
|
||||
### Content Configuration Checksum
|
||||
|
||||
The source-controller calculates the SHA256 checksum of the various
|
||||
configurations of the OCIRepository that indicate a change in source and
|
||||
records it in `.status.contentConfigChecksum`. This field is used to determine
|
||||
if the source artifact needs to be rebuilt.
|
||||
|
||||
### Observed Generation
|
||||
|
||||
The source-controller reports an [observed generation][typical-status-properties]
|
||||
in the OCIRepository's `.status.observedGeneration`. The observed generation is
|
||||
the latest `.metadata.generation` which resulted in either a [ready state](#ready-ocirepository),
|
||||
or stalled due to error it can not recover from without human
|
||||
intervention.
|
||||
|
||||
### Last Handled Reconcile At
|
||||
|
||||
The source-controller reports the last `reconcile.fluxcd.io/requestedAt`
|
||||
annotation value it acted on in the `.status.lastHandledReconcileAt` field.
|
||||
|
||||
For practical information about this field, see [triggering a
|
||||
reconcile](#triggering-a-reconcile).
|
||||
|
||||
[typical-status-properties]: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties
|
||||
[kstatus-spec]: https://github.com/kubernetes-sigs/cli-utils/tree/master/pkg/kstatus
|
||||
[image-pull-secrets]: https://kubernetes.io/docs/concepts/containers/images/#specifying-imagepullsecrets-on-a-pod
|
||||
[image-auto-provider-secrets]: https://fluxcd.io/docs/guides/image-update/#imagerepository-cloud-providers-authentication
|
||||
[pem-encoding]: https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail
|
||||
[sops-guide]: https://fluxcd.io/docs/guides/mozilla-sops/
|
77
go.mod
77
go.mod
|
@ -27,7 +27,7 @@ require (
|
|||
github.com/ProtonMail/go-crypto v0.0.0-20220623141421-5afb4c282135
|
||||
github.com/cyphar/filepath-securejoin v0.2.3
|
||||
github.com/darkowlzz/controller-check v0.0.0-20220325122359-11f5827b7981
|
||||
github.com/distribution/distribution/v3 v3.0.0-20220702071910-8857a1948739
|
||||
github.com/distribution/distribution/v3 v3.0.0-20220729163034-26163d82560f
|
||||
github.com/docker/cli v20.10.17+incompatible
|
||||
github.com/docker/go-units v0.4.0
|
||||
github.com/elazarl/goproxy v0.0.0-20220529153421-8ea89ba92021
|
||||
|
@ -37,6 +37,7 @@ require (
|
|||
github.com/fluxcd/pkg/gitutil v0.1.0
|
||||
github.com/fluxcd/pkg/helmtestserver v0.7.4
|
||||
github.com/fluxcd/pkg/lockedfile v0.1.0
|
||||
github.com/fluxcd/pkg/oci v0.3.0
|
||||
github.com/fluxcd/pkg/runtime v0.16.2
|
||||
github.com/fluxcd/pkg/ssh v0.5.0
|
||||
github.com/fluxcd/pkg/testserver v0.2.0
|
||||
|
@ -55,7 +56,7 @@ require (
|
|||
github.com/prometheus/client_golang v1.12.2
|
||||
github.com/spf13/pflag v1.0.5
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
|
||||
golang.org/x/net v0.0.0-20220706163947-c90051bbdb60
|
||||
golang.org/x/net v0.0.0-20220708220712-1185a9018129
|
||||
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f
|
||||
google.golang.org/api v0.86.0
|
||||
gotest.tools v2.2.0+incompatible
|
||||
|
@ -69,6 +70,11 @@ require (
|
|||
sigs.k8s.io/yaml v1.3.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/go-containerregistry v0.10.0
|
||||
github.com/google/go-containerregistry/pkg/authn/k8schain v0.0.0-20220712174516-ddd39fb9c385
|
||||
)
|
||||
|
||||
// Fix CVE-2022-28948
|
||||
replace gopkg.in/yaml.v3 => gopkg.in/yaml.v3 v3.0.1
|
||||
|
||||
|
@ -82,8 +88,17 @@ require (
|
|||
cloud.google.com/go v0.102.1 // indirect
|
||||
cloud.google.com/go/compute v1.7.0 // indirect
|
||||
cloud.google.com/go/iam v0.3.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go v65.0.0+incompatible // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest/autorest v0.11.27 // indirect
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.20 // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
|
||||
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1 // indirect
|
||||
github.com/BurntSushi/toml v1.0.0 // indirect
|
||||
github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd // indirect
|
||||
|
@ -91,19 +106,35 @@ require (
|
|||
github.com/Masterminds/sprig/v3 v3.2.2 // indirect
|
||||
github.com/Masterminds/squirrel v1.5.3 // indirect
|
||||
github.com/Microsoft/go-winio v0.5.2 // indirect
|
||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d // indirect
|
||||
github.com/acomagu/bufpipe v1.0.3 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect
|
||||
github.com/aws/aws-sdk-go v1.44.53 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.16.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.15.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.12.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.17.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.13.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.11.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.16.6 // indirect
|
||||
github.com/aws/smithy-go v1.11.2 // indirect
|
||||
github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20220517224237-e6f29200ae04 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bshuster-repo/logrus-logstash-hook v1.0.2 // indirect
|
||||
github.com/bugsnag/bugsnag-go v2.1.2+incompatible // indirect
|
||||
github.com/bugsnag/panicwrap v1.3.4 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 // indirect
|
||||
github.com/chrismellard/docker-credential-acr-env v0.0.0-20220327082430-c57b701bfc08 // indirect
|
||||
github.com/containerd/containerd v1.6.6 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.11.4 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dimchansky/utfbom v1.1.1 // indirect
|
||||
github.com/docker/distribution v2.8.1+incompatible // indirect
|
||||
github.com/docker/docker v20.10.17+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.6.4 // indirect
|
||||
|
@ -112,7 +143,7 @@ require (
|
|||
github.com/docker/go-metrics v0.0.1 // indirect
|
||||
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||
github.com/emicklei/go-restful v2.9.5+incompatible // indirect
|
||||
github.com/emicklei/go-restful v2.15.0+incompatible // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
|
||||
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
|
||||
|
@ -125,18 +156,20 @@ require (
|
|||
github.com/go-gorp/gorp/v3 v3.0.2 // indirect
|
||||
github.com/go-logr/zapr v1.2.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.19.5 // indirect
|
||||
github.com/go-openapi/swag v0.19.14 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.0 // indirect
|
||||
github.com/go-openapi/swag v0.21.1 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/gofrs/uuid v4.2.0+incompatible // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.1+incompatible // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.4.1 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/gomodule/redigo v1.8.2 // indirect
|
||||
github.com/google/btree v1.0.1 // indirect
|
||||
github.com/google/gnostic v0.5.7-v3refs // indirect
|
||||
github.com/google/gnostic v0.6.9 // indirect
|
||||
github.com/google/go-cmp v0.5.8 // indirect
|
||||
github.com/google/go-containerregistry/pkg/authn/kubernetes v0.0.0-20220523143934-b17c48b086b7 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.1.0 // indirect
|
||||
|
@ -145,26 +178,27 @@ require (
|
|||
github.com/gorilla/handlers v1.5.1 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/gosuri/uitable v0.0.4 // indirect
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
|
||||
github.com/huandu/xstrings v1.3.2 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/jmoiron/sqlx v1.3.5 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/klauspost/compress v1.13.6 // indirect
|
||||
github.com/klauspost/compress v1.15.4 // indirect
|
||||
github.com/klauspost/cpuid v1.3.1 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/lib/pq v1.10.6 // indirect
|
||||
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.9 // indirect
|
||||
|
@ -184,9 +218,9 @@ require (
|
|||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 // indirect
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 // indirect
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
|
@ -194,13 +228,14 @@ require (
|
|||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/rs/xid v1.2.1 // indirect
|
||||
github.com/rubenv/sql-migrate v1.1.2 // indirect
|
||||
github.com/russross/blackfriday v1.5.2 // indirect
|
||||
github.com/russross/blackfriday v1.6.0 // indirect
|
||||
github.com/sergi/go-diff v1.2.0 // indirect
|
||||
github.com/shopspring/decimal v1.2.0 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||
github.com/spf13/cast v1.4.1 // indirect
|
||||
github.com/spf13/cobra v1.5.0 // indirect
|
||||
github.com/stretchr/testify v1.7.4 // indirect
|
||||
github.com/vbatts/tar-split v0.11.2 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.1 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
|
@ -214,11 +249,11 @@ require (
|
|||
go.uber.org/atomic v1.7.0 // indirect
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
go.uber.org/zap v1.21.0 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2 // indirect
|
||||
golang.org/x/sys v0.0.0-20220624220833-87e55d714810 // indirect
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20220718184931-c8730f7fcb92 // indirect
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
|
||||
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
|
||||
golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
|
@ -235,10 +270,10 @@ require (
|
|||
k8s.io/cli-runtime v0.24.2 // indirect
|
||||
k8s.io/component-base v0.24.2 // indirect
|
||||
k8s.io/klog/v2 v2.60.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20220413171646-5e7f5fdc6da6 // indirect
|
||||
k8s.io/kubectl v0.24.2 // indirect
|
||||
oras.land/oras-go v1.2.0 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20220525155127-227cbc7cc124 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.11.4 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.13.6 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
|
||||
|
|
|
@ -36,6 +36,7 @@ function cleanup(){
|
|||
kubectl -n kube-system describe pods
|
||||
kubectl -n source-system describe pods
|
||||
kubectl -n source-system get gitrepositories -oyaml
|
||||
kubectl -n source-system get ocirepositories -oyaml
|
||||
kubectl -n source-system get helmrepositories -oyaml
|
||||
kubectl -n source-system get helmcharts -oyaml
|
||||
kubectl -n source-system get all
|
||||
|
@ -72,6 +73,7 @@ echo "Run smoke tests"
|
|||
kubectl -n source-system apply -f "${ROOT_DIR}/config/samples"
|
||||
kubectl -n source-system rollout status deploy/source-controller --timeout=1m
|
||||
kubectl -n source-system wait gitrepository/gitrepository-sample --for=condition=ready --timeout=1m
|
||||
kubectl -n source-system wait ocirepository/ocirepository-sample --for=condition=ready --timeout=1m
|
||||
kubectl -n source-system wait helmrepository/helmrepository-sample --for=condition=ready --timeout=1m
|
||||
kubectl -n source-system wait helmchart/helmchart-sample --for=condition=ready --timeout=1m
|
||||
kubectl -n source-system delete -f "${ROOT_DIR}/config/samples"
|
||||
|
|
13
main.go
13
main.go
|
@ -309,6 +309,19 @@ func main() {
|
|||
setupLog.Error(err, "unable to create controller", "controller", "Bucket")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err = (&controllers.OCIRepositoryReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Storage: storage,
|
||||
EventRecorder: eventRecorder,
|
||||
ControllerName: controllerName,
|
||||
Metrics: metricsH,
|
||||
}).SetupWithManagerAndOptions(mgr, controllers.OCIRepositoryReconcilerOptions{
|
||||
MaxConcurrentReconciles: concurrent,
|
||||
RateLimiter: helper.GetRateLimiter(rateLimiterOptions),
|
||||
}); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "OCIRepository")
|
||||
os.Exit(1)
|
||||
}
|
||||
// +kubebuilder:scaffold:builder
|
||||
|
||||
go func() {
|
||||
|
|
Loading…
Reference in New Issue