Merge pull request #788 from fluxcd/oci

[RFC-0003] Implement OCIRepository reconciliation
This commit is contained in:
Stefan Prodan 2022-08-08 15:59:00 +03:00 committed by GitHub
commit 1db1626fe1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 5215 additions and 77 deletions

View File

@ -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

View File

@ -25,4 +25,7 @@ resources:
- group: source
kind: Bucket
version: v1beta1
- group: source
kind: OCIRepository
version: v1beta2
version: "2"

View File

@ -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

View File

@ -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{})
}

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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: []

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 := &registryClientTestServer{}
// 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))
}

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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 &lsquo;aws&rsquo;, &lsquo;azure&rsquo;, &lsquo;gcp&rsquo; or &lsquo;generic&rsquo;.
When not specified, defaults to &lsquo;generic&rsquo;.</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&rsquo;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 &lsquo;sha256:<HASH>&rsquo;.</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 &lsquo;aws&rsquo;, &lsquo;azure&rsquo;, &lsquo;gcp&rsquo; or &lsquo;generic&rsquo;.
When not specified, defaults to &lsquo;generic&rsquo;.</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.

View File

@ -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)

View File

@ -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
View File

@ -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

537
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -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
View File

@ -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() {