Add SigningKey to CommitSpec
Signed-off-by: LWJ <lwjames1996@gmail.com>
This commit is contained in:
parent
daad724ad2
commit
4aa56f1013
|
|
@ -106,6 +106,9 @@ type CommitSpec struct {
|
||||||
// AuthorEmail gives the email to provide when making a commit
|
// AuthorEmail gives the email to provide when making a commit
|
||||||
// +required
|
// +required
|
||||||
AuthorEmail string `json:"authorEmail"`
|
AuthorEmail string `json:"authorEmail"`
|
||||||
|
// SigningKey provides the option to sign commits with a GPG key
|
||||||
|
// +optional
|
||||||
|
SigningKey *SigningKey `json:"signingKey,omitempty"`
|
||||||
// MessageTemplate provides a template for the commit message,
|
// MessageTemplate provides a template for the commit message,
|
||||||
// into which will be interpolated the details of the change made.
|
// into which will be interpolated the details of the change made.
|
||||||
// +optional
|
// +optional
|
||||||
|
|
@ -142,6 +145,16 @@ type ImageUpdateAutomationStatus struct {
|
||||||
meta.ReconcileRequestStatus `json:",inline"`
|
meta.ReconcileRequestStatus `json:",inline"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SigningKey references a Kubernetes secret that contains a GPG keypair
|
||||||
|
type SigningKey struct {
|
||||||
|
// SecretRef holds the name to a secret that contains a 'value' key
|
||||||
|
// with the ASCII Armored file (.asc) containing the GPG signing
|
||||||
|
// keypair as the value. It must be in the same namespace as the
|
||||||
|
// ImageUpdateAutomation.
|
||||||
|
// +required
|
||||||
|
SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// GitNotAvailableReason is used for ConditionReady when the
|
// GitNotAvailableReason is used for ConditionReady when the
|
||||||
// automation run cannot proceed because the git repository is
|
// automation run cannot proceed because the git repository is
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ limitations under the License.
|
||||||
package v1alpha1
|
package v1alpha1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/fluxcd/pkg/apis/meta"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||||
)
|
)
|
||||||
|
|
@ -28,6 +29,11 @@ import (
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *CommitSpec) DeepCopyInto(out *CommitSpec) {
|
func (in *CommitSpec) DeepCopyInto(out *CommitSpec) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
if in.SigningKey != nil {
|
||||||
|
in, out := &in.SigningKey, &out.SigningKey
|
||||||
|
*out = new(SigningKey)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommitSpec.
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommitSpec.
|
||||||
|
|
@ -125,7 +131,7 @@ func (in *ImageUpdateAutomationSpec) DeepCopyInto(out *ImageUpdateAutomationSpec
|
||||||
*out = new(UpdateStrategy)
|
*out = new(UpdateStrategy)
|
||||||
**out = **in
|
**out = **in
|
||||||
}
|
}
|
||||||
out.Commit = in.Commit
|
in.Commit.DeepCopyInto(&out.Commit)
|
||||||
if in.Push != nil {
|
if in.Push != nil {
|
||||||
in, out := &in.Push, &out.Push
|
in, out := &in.Push, &out.Push
|
||||||
*out = new(PushSpec)
|
*out = new(PushSpec)
|
||||||
|
|
@ -189,6 +195,26 @@ func (in *PushSpec) DeepCopy() *PushSpec {
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *SigningKey) DeepCopyInto(out *SigningKey) {
|
||||||
|
*out = *in
|
||||||
|
if in.SecretRef != nil {
|
||||||
|
in, out := &in.SecretRef, &out.SecretRef
|
||||||
|
*out = new(meta.LocalObjectReference)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SigningKey.
|
||||||
|
func (in *SigningKey) DeepCopy() *SigningKey {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(SigningKey)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *UpdateStrategy) DeepCopyInto(out *UpdateStrategy) {
|
func (in *UpdateStrategy) DeepCopyInto(out *UpdateStrategy) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,23 @@ spec:
|
||||||
message, into which will be interpolated the details of the
|
message, into which will be interpolated the details of the
|
||||||
change made.
|
change made.
|
||||||
type: string
|
type: string
|
||||||
|
signingKey:
|
||||||
|
description: SigningKey provides the option to sign commits with
|
||||||
|
a GPG key
|
||||||
|
properties:
|
||||||
|
secretRef:
|
||||||
|
description: SecretRef holds the name to a secret that contains
|
||||||
|
a 'value' key with the ASCII Armored file (.asc) containing
|
||||||
|
the GPG signing keypair as the value. It must be in the
|
||||||
|
same namespace as the ImageUpdateAutomation.
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
description: Name of the referent
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
required:
|
required:
|
||||||
- authorEmail
|
- authorEmail
|
||||||
- authorName
|
- authorName
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,11 @@ limitations under the License.
|
||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"golang.org/x/crypto/openpgp"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -227,10 +229,15 @@ func (r *ImageUpdateAutomationReconciler) Reconcile(ctx context.Context, req ctr
|
||||||
|
|
||||||
var statusMessage string
|
var statusMessage string
|
||||||
|
|
||||||
|
var signingEntity *openpgp.Entity
|
||||||
|
if auto.Spec.Commit.SigningKey != nil {
|
||||||
|
signingEntity, err = r.getSigningEntity(ctx, auto)
|
||||||
|
}
|
||||||
|
|
||||||
// The status message depends on what happens next. Since there's
|
// The status message depends on what happens next. Since there's
|
||||||
// more than one way to succeed, there's some if..else below, and
|
// more than one way to succeed, there's some if..else below, and
|
||||||
// early returns only on failure.
|
// early returns only on failure.
|
||||||
if rev, err := commitAll(ctx, repo, &auto.Spec.Commit, templateValues); err != nil {
|
if rev, err := commitAll(repo, &auto.Spec.Commit, templateValues, signingEntity); err != nil {
|
||||||
if err == errNoChanges {
|
if err == errNoChanges {
|
||||||
r.event(ctx, auto, events.EventSeverityInfo, "no updates made")
|
r.event(ctx, auto, events.EventSeverityInfo, "no updates made")
|
||||||
log.V(debug).Info("no changes made in working directory; no commit")
|
log.V(debug).Info("no changes made in working directory; no commit")
|
||||||
|
|
@ -439,7 +446,7 @@ func switchBranch(repo *gogit.Repository, pushBranch string) error {
|
||||||
|
|
||||||
var errNoChanges error = errors.New("no changes made to working directory")
|
var errNoChanges error = errors.New("no changes made to working directory")
|
||||||
|
|
||||||
func commitAll(ctx context.Context, repo *gogit.Repository, commit *imagev1.CommitSpec, values TemplateData) (string, error) {
|
func commitAll(repo *gogit.Repository, commit *imagev1.CommitSpec, values TemplateData, ent *openpgp.Entity) (string, error) {
|
||||||
working, err := repo.Worktree()
|
working, err := repo.Worktree()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|
@ -473,6 +480,7 @@ func commitAll(ctx context.Context, repo *gogit.Repository, commit *imagev1.Comm
|
||||||
Email: commit.AuthorEmail,
|
Email: commit.AuthorEmail,
|
||||||
When: time.Now(),
|
When: time.Now(),
|
||||||
},
|
},
|
||||||
|
SignKey: ent,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
@ -480,6 +488,36 @@ func commitAll(ctx context.Context, repo *gogit.Repository, commit *imagev1.Comm
|
||||||
return rev.String(), nil
|
return rev.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getSigningEntity retrieves an OpenPGP entity referenced by the
|
||||||
|
// provided imagev1.ImageUpdateAutomation for git commit signing
|
||||||
|
func (r *ImageUpdateAutomationReconciler) getSigningEntity(ctx context.Context, auto imagev1.ImageUpdateAutomation) (*openpgp.Entity, error) {
|
||||||
|
// get kubernetes secret
|
||||||
|
secretName := types.NamespacedName{
|
||||||
|
Namespace: auto.GetNamespace(),
|
||||||
|
Name: auto.Spec.Commit.SigningKey.SecretRef.Name,
|
||||||
|
}
|
||||||
|
var secret corev1.Secret
|
||||||
|
if err := r.Get(ctx, secretName, &secret); err != nil {
|
||||||
|
return nil, fmt.Errorf("could not find signing key secret '%s': %w", secretName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get data from secret
|
||||||
|
data, ok := secret.Data["value"]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("signing key secret '%s' does not contain a 'value' key", secretName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// read entity from secret value
|
||||||
|
entities, err := openpgp.ReadArmoredKeyRing(bytes.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not read signing key from secret '%s': %w", secretName, err)
|
||||||
|
}
|
||||||
|
if len(entities) > 1 {
|
||||||
|
return nil, fmt.Errorf("multiple entities read from secret '%s', could not determine which signing key to use", secretName)
|
||||||
|
}
|
||||||
|
return entities[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
// push pushes the branch given to the origin using the git library
|
// push pushes the branch given to the origin using the git library
|
||||||
// indicated by `impl`. It's passed both the path to the repo and a
|
// indicated by `impl`. It's passed both the path to the repo and a
|
||||||
// gogit.Repository value, since the latter may as well be used if the
|
// gogit.Repository value, since the latter may as well be used if the
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"golang.org/x/crypto/openpgp"
|
||||||
|
"golang.org/x/crypto/openpgp/armor"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
@ -388,6 +390,161 @@ Images:
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Context("commit signing", func() {
|
||||||
|
|
||||||
|
var localRepo *git.Repository
|
||||||
|
|
||||||
|
// generate keypair for signing
|
||||||
|
pgpEntity, err := openpgp.NewEntity("", "", "", nil)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
Expect(initGitRepo(gitServer, "testdata/appconfig", branch, repositoryPath)).To(Succeed())
|
||||||
|
repoURL := gitServer.HTTPAddressWithCredentials() + repositoryPath
|
||||||
|
var err error
|
||||||
|
localRepo, err = git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{
|
||||||
|
URL: repoURL,
|
||||||
|
RemoteName: "origin",
|
||||||
|
ReferenceName: plumbing.NewBranchReferenceName(branch),
|
||||||
|
})
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
gitRepoKey := types.NamespacedName{
|
||||||
|
Name: "image-auto-" + randStringRunes(5),
|
||||||
|
Namespace: namespace.Name,
|
||||||
|
}
|
||||||
|
gitRepo := &sourcev1.GitRepository{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: gitRepoKey.Name,
|
||||||
|
Namespace: namespace.Name,
|
||||||
|
},
|
||||||
|
Spec: sourcev1.GitRepositorySpec{
|
||||||
|
URL: repoURL,
|
||||||
|
Interval: metav1.Duration{Duration: time.Minute},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Expect(k8sClient.Create(context.Background(), gitRepo)).To(Succeed())
|
||||||
|
policyKey := types.NamespacedName{
|
||||||
|
Name: "policy-" + randStringRunes(5),
|
||||||
|
Namespace: namespace.Name,
|
||||||
|
}
|
||||||
|
// NB not testing the image reflector controller; this
|
||||||
|
// will make a "fully formed" ImagePolicy object.
|
||||||
|
policy := &imagev1_reflect.ImagePolicy{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: policyKey.Name,
|
||||||
|
Namespace: policyKey.Namespace,
|
||||||
|
},
|
||||||
|
Spec: imagev1_reflect.ImagePolicySpec{
|
||||||
|
ImageRepositoryRef: meta.LocalObjectReference{
|
||||||
|
Name: "not-expected-to-exist",
|
||||||
|
},
|
||||||
|
Policy: imagev1_reflect.ImagePolicyChoice{
|
||||||
|
SemVer: &imagev1_reflect.SemVerPolicy{
|
||||||
|
Range: "1.x",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Status: imagev1_reflect.ImagePolicyStatus{
|
||||||
|
LatestImage: "helloworld:v1.0.0",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Expect(k8sClient.Create(context.Background(), policy)).To(Succeed())
|
||||||
|
Expect(k8sClient.Status().Update(context.Background(), policy)).To(Succeed())
|
||||||
|
|
||||||
|
// Insert a setter reference into the deployment file,
|
||||||
|
// before creating the automation object itself.
|
||||||
|
commitInRepo(repoURL, branch, "Install setter marker", func(tmp string) {
|
||||||
|
replaceMarker(tmp, policyKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
// pull the head commit we just pushed, so it's not
|
||||||
|
// considered a new commit when checking for a commit
|
||||||
|
// made by automation.
|
||||||
|
waitForNewHead(localRepo, branch)
|
||||||
|
|
||||||
|
// now create the automation object, and let it (one
|
||||||
|
// hopes!) make a commit itself.
|
||||||
|
updateKey := types.NamespacedName{
|
||||||
|
Namespace: namespace.Name,
|
||||||
|
Name: "update-test",
|
||||||
|
}
|
||||||
|
updateBySetters := &imagev1.ImageUpdateAutomation{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: updateKey.Name,
|
||||||
|
Namespace: updateKey.Namespace,
|
||||||
|
},
|
||||||
|
Spec: imagev1.ImageUpdateAutomationSpec{
|
||||||
|
Interval: metav1.Duration{Duration: 2 * time.Hour}, // this is to ensure any subsequent run should be outside the scope of the testing
|
||||||
|
Checkout: imagev1.GitCheckoutSpec{
|
||||||
|
GitRepositoryRef: meta.LocalObjectReference{
|
||||||
|
Name: gitRepoKey.Name,
|
||||||
|
},
|
||||||
|
Branch: branch,
|
||||||
|
},
|
||||||
|
Update: &imagev1.UpdateStrategy{
|
||||||
|
Strategy: imagev1.UpdateStrategySetters,
|
||||||
|
},
|
||||||
|
Commit: imagev1.CommitSpec{
|
||||||
|
SigningKey: &imagev1.SigningKey{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// configure OpenPGP armor encoder
|
||||||
|
b := bytes.NewBuffer(nil)
|
||||||
|
w, err := armor.Encode(b, openpgp.PrivateKeyType, nil)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
// serialize private key
|
||||||
|
err = pgpEntity.SerializePrivate(w, nil)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
err = w.Close()
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
// create the secret containing signing key
|
||||||
|
sec := &corev1.Secret{
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"value": b.Bytes(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
sec.Name = "signing-key-secret-" + randStringRunes(5)
|
||||||
|
sec.Namespace = namespace.Name
|
||||||
|
Expect(k8sClient.Create(context.Background(), sec)).To(Succeed())
|
||||||
|
updateBySetters.Spec.Commit.SigningKey.SecretRef = &meta.LocalObjectReference{Name: sec.Name}
|
||||||
|
|
||||||
|
Expect(k8sClient.Create(context.Background(), updateBySetters)).To(Succeed())
|
||||||
|
// wait for a new commit to be made by the controller
|
||||||
|
waitForNewHead(localRepo, branch)
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterEach(func() {
|
||||||
|
Expect(k8sClient.Delete(context.Background(), namespace)).To(Succeed())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("signs the commit with the generated GPG key", func() {
|
||||||
|
head, _ := localRepo.Head()
|
||||||
|
commit, err := localRepo.CommitObject(head.Hash())
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
// configure OpenPGP armor encoder
|
||||||
|
b := bytes.NewBuffer(nil)
|
||||||
|
w, err := armor.Encode(b, openpgp.PublicKeyType, nil)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
// serialize public key
|
||||||
|
err = pgpEntity.Serialize(w)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
err = w.Close()
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
// verify commit
|
||||||
|
ent, err := commit.Verify(b.String())
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(ent.PrimaryKey.Fingerprint).To(Equal(pgpEntity.PrimaryKey.Fingerprint))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
endToEnd := func(impl, proto string) func() {
|
endToEnd := func(impl, proto string) func() {
|
||||||
return func() {
|
return func() {
|
||||||
var (
|
var (
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,20 @@ string
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
|
<code>signingKey</code><br>
|
||||||
|
<em>
|
||||||
|
<a href="#image.toolkit.fluxcd.io/v1alpha1.SigningKey">
|
||||||
|
SigningKey
|
||||||
|
</a>
|
||||||
|
</em>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<em>(Optional)</em>
|
||||||
|
<p>SigningKey provides the option to sign commits with a GPG key</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
<code>messageTemplate</code><br>
|
<code>messageTemplate</code><br>
|
||||||
<em>
|
<em>
|
||||||
string
|
string
|
||||||
|
|
@ -502,6 +516,40 @@ starting point, if it doesn’t already exist.</p>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<h3 id="image.toolkit.fluxcd.io/v1alpha1.SigningKey">SigningKey
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
(<em>Appears on:</em>
|
||||||
|
<a href="#image.toolkit.fluxcd.io/v1alpha1.CommitSpec">CommitSpec</a>)
|
||||||
|
</p>
|
||||||
|
<p>SigningKey references a Kubernetes secret that contains a GPG file</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>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 holds the name to a secret that contains a ‘value’ key with the GPG file as the value. It must be in the same namespace as the ImageUpdateAutomation.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<h3 id="image.toolkit.fluxcd.io/v1alpha1.UpdateStrategy">UpdateStrategy
|
<h3 id="image.toolkit.fluxcd.io/v1alpha1.UpdateStrategy">UpdateStrategy
|
||||||
</h3>
|
</h3>
|
||||||
<p>
|
<p>
|
||||||
|
|
|
||||||
1
go.mod
1
go.mod
|
|
@ -26,6 +26,7 @@ require (
|
||||||
github.com/onsi/gomega v1.10.2
|
github.com/onsi/gomega v1.10.2
|
||||||
github.com/otiai10/copy v1.2.0
|
github.com/otiai10/copy v1.2.0
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
|
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
|
||||||
k8s.io/api v0.20.2
|
k8s.io/api v0.20.2
|
||||||
k8s.io/apimachinery v0.20.2
|
k8s.io/apimachinery v0.20.2
|
||||||
k8s.io/client-go v0.20.2
|
k8s.io/client-go v0.20.2
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue