Skip to content

Commit

Permalink
Check Flux sources for artifact to avoid spamming the log (#359)
Browse files Browse the repository at this point in the history
* Check for an artifact in Flux sources

When a Stack refers to a Flux source, we check the latter for a `Ready`
condition, and if present and false, bail on reconciliation until it
gets requeued by the watch mechanism.

A flaw in this is that a new source will have no Ready condition (true
or false), so the controller will continue, only to find there's no
record of what it should download. Instead of gracefully parking itself,
it bounces off this until the source has been seen by the
source-controller.

So: check that there's an artifact.

* Use github.com/fluxcd/pkg/http/fetch

This obviates a bunch of my own code (and is a bit more careful with
buffers).

* Add changelog entry

Signed-off-by: Michael Bridgen <mbridgen@pulumi.com>
  • Loading branch information
squaremo authored Nov 11, 2022
1 parent 36e3249 commit ef7bd77
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 179 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ CHANGELOG
[#369](https://github.com/pulumi/pulumi-kubernetes-operator/pull/369)
- Use an init process so processes spawned by `pulumi` are reaped
[#367](https://github.com/pulumi/pulumi-kubernetes-operator/pull/367)
- When a Stack uses a Flux source, but the source has no artifact to download, park the Stack until
the source has been updated, rather than retrying
[#359](https://github.com/pulumi/pulumi-kubernetes-operator/pull/359)

## 1.10.1 (2022-10-25)

Expand Down
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ require (
)

require (
github.com/fluxcd/pkg/http/fetch v0.2.0
github.com/go-git/go-git/v5 v5.4.2
github.com/onsi/ginkgo/v2 v2.3.1
sigs.k8s.io/yaml v1.2.0
Expand All @@ -61,10 +62,12 @@ require (
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/cheggaaa/pb v1.0.18 // indirect
github.com/coreos/prometheus-operator v0.38.1-0.20200424145508-7e176fda06cc // indirect
github.com/cyphar/filepath-securejoin v0.2.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/djherbis/times v1.2.0 // indirect
github.com/emirpasic/gods v1.12.0 // indirect
github.com/evanphx/json-patch v4.11.0+incompatible // indirect
github.com/fluxcd/pkg/tar v0.2.0 // indirect
github.com/form3tech-oss/jwt-go v3.2.2+incompatible // indirect
github.com/go-git/gcfg v1.5.0 // indirect
github.com/go-git/go-billy/v5 v5.3.1 // indirect
Expand All @@ -78,7 +81,9 @@ require (
github.com/googleapis/gnostic v0.5.5 // indirect
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.1 // indirect
github.com/hashicorp/go-multierror v1.0.0 // indirect
github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
Expand Down
14 changes: 14 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,8 @@ github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7Do
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=
github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI=
github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8=
github.com/cznic/fileutil v0.0.0-20180108211300-6a051e75936f/go.mod h1:8S58EK26zhXSxzv7NQFpnliaOQsmDUxvoQO3rt154Vg=
github.com/cznic/golex v0.0.0-20170803123110-4ab7c5e190e4/go.mod h1:+bmmJDNmKlhWNG+gwWCkaBoTy39Fs+bzRxVBzoTQbIc=
Expand Down Expand Up @@ -306,6 +308,12 @@ github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL
github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc=
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/structtag v1.1.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
github.com/fluxcd/pkg/http/fetch v0.2.0 h1:Ss2bPfEn1e7OLebzkrU2c2bT1vZWFZmHCjnl0ACsRYM=
github.com/fluxcd/pkg/http/fetch v0.2.0/go.mod h1:60QOWiz4pLe8SPtlHZtVo92qga18qQT2PrbU0D5NWqM=
github.com/fluxcd/pkg/tar v0.2.0 h1:HEUHgONQYsJGeZZ4x6h5nQU9Aox1I4T3bOp1faWTqf8=
github.com/fluxcd/pkg/tar v0.2.0/go.mod h1:w0/TOC7kwBJhnSJn7TCABkc/I7ib1f2Yz6vOsbLBnhw=
github.com/fluxcd/pkg/testserver v0.3.0 h1:oyZW6YWHVZR7FRVNu7lN9F5H808TD2jCzBm8CenFoi0=
github.com/fluxcd/pkg/testserver v0.3.0/go.mod h1:gjOKX41okmrGYOa4oOF2fiLedDAfPo1XaG/EzrUUGBI=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
Expand Down Expand Up @@ -569,7 +577,10 @@ github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FK
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.1.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
Expand All @@ -578,6 +589,8 @@ github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ=
github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
Expand Down Expand Up @@ -812,6 +825,7 @@ github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je4
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo=
github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc=
github.com/onsi/gomega v1.22.0 h1:AIg2/OntwkBiCg5Tt1ayyiF1ArFrWFoCSMtMi/wdApk=
github.com/onsi/gomega v1.22.0/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc=
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
Expand Down
83 changes: 29 additions & 54 deletions pkg/controller/stack/flux.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,21 @@
package stack

import (
"bytes"
"context"
"crypto/sha1"
"crypto/sha256"
"fmt"
"io"
"net/http"
"os"
"path/filepath"

"github.com/fluxcd/pkg/http/fetch"

"github.com/pulumi/pulumi/sdk/v3/go/auto"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

"github.com/pulumi/pulumi-kubernetes-operator/pkg/apis/pulumi/shared"
)

const maxArtifactDownloadSize = 50 * 1024 * 1024

func (sess *reconcileStackSession) SetupWorkdirFromFluxSource(ctx context.Context, source unstructured.Unstructured, fluxSource *shared.FluxSource) (_commit string, retErr error) {
rootdir, err := os.MkdirTemp("", "pulumi_source")
if err != nil {
Expand Down Expand Up @@ -57,38 +56,9 @@ func (sess *reconcileStackSession) SetupWorkdirFromFluxSource(ctx context.Contex
return "", err
}

req, err := http.NewRequestWithContext(ctx, http.MethodGet, artifactURL, nil)
if err != nil {
return "", fmt.Errorf("failed to create a request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", fmt.Errorf("request for artifact failed: %w", err)
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("failed to download artifact from %s, status %q (expected 200 OK)", artifactURL, resp.Status)
}
// TODO validate size, if given

defer resp.Body.Close()

var buf bytes.Buffer
hasher := sha256.New()
if len(checksum) == 40 { // Flux source-controller <= 0.17.2 used SHA1
hasher = sha1.New()
}
out := io.MultiWriter(hasher, &buf)
if _, err := io.Copy(out, resp.Body); err != nil {
return "", fmt.Errorf("failed to compute checksum from artifact response: %w", err)
}
if checksum1 := fmt.Sprintf("%x", hasher.Sum(nil)); checksum1 != checksum {
return "", fmt.Errorf("computed checksum of artifact %q does not match checksum recorded %q", checksum1, checksum)
}

// we downloaded the artifact gzip-tarball into a buffer and it matches the checksum; untar it
// into our working dir.
if err = untar(&buf, rootdir); err != nil {
return "", fmt.Errorf("failed to extract archive tarball: %w", err)
fetcher := fetch.NewArchiveFetcher(1, maxArtifactDownloadSize, maxArtifactDownloadSize*10, "")
if err = fetcher.Fetch(artifactURL, checksum, rootdir); err != nil {
return "", fmt.Errorf("failed to get artifact from source: %w", err)
}

// woo! now there's a directory with source in `rootdir`. Construct a workspace.
Expand All @@ -107,25 +77,30 @@ func (sess *reconcileStackSession) SetupWorkdirFromFluxSource(ctx context.Contex
// ready, and nil if it cannot determine so.
func checkFluxSourceReady(obj unstructured.Unstructured) error {
conditions, ok, err := unstructured.NestedSlice(obj.Object, "status", "conditions")
if err != nil || !ok {
// didn't find a []Condition, so there's nothing to indicate that it's not ready
return nil
}
for _, c0 := range conditions {
var c map[string]interface{}
if c, ok = c0.(map[string]interface{}); !ok {
// condition isn't the right shape, move on
continue
}
if t, ok, err := unstructured.NestedString(c, "type"); ok && err == nil && t == "Ready" {
if v, ok, err := unstructured.NestedString(c, "status"); ok && err == nil && v == "True" {
// found the Ready condition and it is actually ready
return nil
if ok && err == nil {
// didn't find a []Condition, so there's nothing to indicate that it's not ready there
for _, c0 := range conditions {
var c map[string]interface{}
if c, ok = c0.(map[string]interface{}); !ok {
// condition isn't the right shape, try the next one
continue
}
if t, ok, err := unstructured.NestedString(c, "type"); ok && err == nil && t == "Ready" {
if v, ok, err := unstructured.NestedString(c, "status"); ok && err == nil && v == "True" {
// found the Ready condition and it is actually ready; proceed to next check
break
}
// found the Ready condition and it's something other than ready
return fmt.Errorf("source Ready condition does not have status True %#v", c)
}
// found the Ready condition and it's something other than ready
return fmt.Errorf("source Ready condition does not have status True %#v", c)
}
// Ready=true, or no ready condition to tell us either way
}

_, ok, err = unstructured.NestedMap(obj.Object, "status", "artifact")
if !ok || err != nil {
return fmt.Errorf(".status.artifact does not have an Artifact object")
}
// didn't find the Ready condition

return nil
}
125 changes: 0 additions & 125 deletions pkg/controller/stack/untar.go

This file was deleted.

28 changes: 28 additions & 0 deletions test/flux_source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,34 @@ var _ = Describe("Flux source integration", func() {
})
})

When("the source object has no artifact", func() {
BeforeEach(func() {
unstructured.RemoveNestedField(source.Object, "status", "artifact")
Expect(k8sClient.Status().Update(context.TODO(), source)).To(Succeed())
stack.Name = "source-no-artifact"
})

It("marks the stack as failed and to be retried", func() {
waitForStackFailure(stack)
expectInProgress(stack.Status.Conditions)

newArtifactRevision := randString()
By("putting the artifact in the status, the stack can run")
artifact := map[string]interface{}{
"path": "irrelevant",
"url": artifactURL,
"revision": newArtifactRevision,
"checksum": artifactChecksum,
}
unstructured.SetNestedMap(source.Object, artifact, "status", "artifact")
Expect(k8sClient.Status().Update(context.TODO(), source)).To(Succeed())

waitForStackSuccess(stack)
expectReady(stack.Status.Conditions)
Expect(stack.Status.LastUpdate.LastSuccessfulCommit).To(Equal(newArtifactRevision))
})
})

When("the checksum is wrong", func() {
BeforeEach(func() {
unstructured.SetNestedField(source.Object, "not-the-right-checksum",
Expand Down

0 comments on commit ef7bd77

Please sign in to comment.