Merge pull request #507 from nnt1054/feature/helm3_provider

Feature/helm3 provider
pull/517/head
Karolis 2020-07-14 10:32:06 +01:00 committed by GitHub
commit 3198921af0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 3929 additions and 1755 deletions

View File

@ -77,7 +77,8 @@ alpha: image
e2e: install
cd tests && go test
run: install
run:
go install github.com/keel-hq/keel/cmd/keel
keel --no-incluster --ui-dir ui/dist
lint-ui:

View File

@ -90,4 +90,3 @@ func (c *ApprovalContext) Created() string {
c.AddHeader(ApprovalCreatedHeader)
return c.v.CreatedAt.String()
}

View File

@ -3,8 +3,8 @@ package hipchat
import (
"time"
log "github.com/sirupsen/logrus"
"github.com/daneharrigan/hipchat"
log "github.com/sirupsen/logrus"
)
type XmppImplementer interface {

View File

@ -11,8 +11,8 @@ import (
"github.com/keel-hq/keel/bot"
"github.com/keel-hq/keel/constants"
log "github.com/sirupsen/logrus"
h "github.com/daneharrigan/hipchat"
log "github.com/sirupsen/logrus"
)
const connectionAttemptsDefault = 5

View File

@ -14,27 +14,27 @@ func (b *Bot) RequestApproval(req *types.Approval) error {
req.Message,
types.LevelSuccess.Color(),
[]slack.AttachmentField{
slack.AttachmentField{
{
Title: "Approval required!",
Value: req.Message + "\n" + fmt.Sprintf("To vote for change type '%s approve %s' to reject it: '%s reject %s'.", b.name, req.Identifier, b.name, req.Identifier),
Short: false,
},
slack.AttachmentField{
{
Title: "Votes",
Value: fmt.Sprintf("%d/%d", req.VotesReceived, req.VotesRequired),
Short: true,
},
slack.AttachmentField{
{
Title: "Delta",
Value: req.Delta(),
Short: true,
},
slack.AttachmentField{
{
Title: "Identifier",
Value: req.Identifier,
Short: true,
},
slack.AttachmentField{
{
Title: "Provider",
Value: req.Provider.String(),
Short: true,
@ -50,22 +50,22 @@ func (b *Bot) ReplyToApproval(approval *types.Approval) error {
"All approvals received, thanks for voting!",
types.LevelInfo.Color(),
[]slack.AttachmentField{
slack.AttachmentField{
{
Title: "vote received!",
Value: "Waiting for remaining votes.",
Short: false,
},
slack.AttachmentField{
{
Title: "Votes",
Value: fmt.Sprintf("%d/%d", approval.VotesReceived, approval.VotesRequired),
Short: true,
},
slack.AttachmentField{
{
Title: "Delta",
Value: approval.Delta(),
Short: true,
},
slack.AttachmentField{
{
Title: "Identifier",
Value: approval.Identifier,
Short: true,
@ -77,27 +77,27 @@ func (b *Bot) ReplyToApproval(approval *types.Approval) error {
"Change was rejected",
types.LevelWarn.Color(),
[]slack.AttachmentField{
slack.AttachmentField{
{
Title: "change rejected",
Value: "Change was rejected.",
Short: false,
},
slack.AttachmentField{
{
Title: "Status",
Value: approval.Status().String(),
Short: true,
},
slack.AttachmentField{
{
Title: "Votes",
Value: fmt.Sprintf("%d/%d", approval.VotesReceived, approval.VotesRequired),
Short: true,
},
slack.AttachmentField{
{
Title: "Delta",
Value: approval.Delta(),
Short: true,
},
slack.AttachmentField{
{
Title: "Identifier",
Value: approval.Identifier,
Short: true,
@ -109,22 +109,22 @@ func (b *Bot) ReplyToApproval(approval *types.Approval) error {
"All approvals received, thanks for voting!",
types.LevelSuccess.Color(),
[]slack.AttachmentField{
slack.AttachmentField{
{
Title: "update approved!",
Value: "All approvals received, thanks for voting!",
Short: false,
},
slack.AttachmentField{
{
Title: "Votes",
Value: fmt.Sprintf("%d/%d", approval.VotesReceived, approval.VotesRequired),
Short: true,
},
slack.AttachmentField{
{
Title: "Delta",
Value: approval.Delta(),
Short: true,
},
slack.AttachmentField{
{
Title: "Identifier",
Value: approval.Identifier,
Short: true,

View File

@ -150,7 +150,7 @@ func (b *Bot) postMessage(title, message, color string, fields []slack.Attachmen
params.IconURL = b.getBotUserIconURL()
attachements := []slack.Attachment{
slack.Attachment{
{
Fallback: message,
Color: color,
Fields: fields,

View File

@ -350,7 +350,7 @@ func TestProcessRejectedReply(t *testing.T) {
}
func TestIsApproval(t *testing.T) {
event := &slack.MessageEvent{
Msg: slack.Msg{
Channel: "approvals",

View File

@ -3,7 +3,7 @@ name: keel
description: Open source, tool for automating Kubernetes deployment updates. Keel is stateless, robust and lightweight.
version: 0.8.22
# Note that we use appVersion to get images tag, so make sure this is correct.
appVersion: 0.16.0
appVersion: 0.16.1
keywords:
- kubernetes deployment
- helm release

View File

@ -1,12 +1,14 @@
{{- if .Values.rbac.enabled }}
apiVersion: rbac.authorization.k8s.io/v1
# apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: {{ template "keel.name" . }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ template "keel.name" . }}
# name: {{ template "keel.name" . }}
name: cluster-admin
subjects:
- kind: ServiceAccount
name: {{ template "serviceAccount.name" . }}

View File

@ -53,27 +53,33 @@ spec:
{{- if .Values.polling.enabled }}
# Enable polling
- name: POLL
value: "1"
value: "true"
{{- else }}
# Disable polling
- name: POLL
value: "0"
value: "false"
{{- end }}
{{- if .Values.helmProvider.enabled }}
{{- if eq .Values.helmProvider.version "v2" }}
# Enable/disable Helm provider
- name: HELM_PROVIDER
value: "1"
value: "true"
- name: TILLER_NAMESPACE
value: "{{ .Values.helmProvider.tillerNamespace }}"
- name: TILLER_ADDRESS
value: "{{ .Values.helmProvider.tillerAddress }}"
{{- else if eq .Values.helmProvider.version "v3" }}
# Enable/disable Helm provider
- name: HELM3_PROVIDER
value: "true"
{{- end }}
{{- end }}
{{- if .Values.gcr.enabled }}
# Enable GCR with pub/sub support
- name: PROJECT_ID
value: "{{ .Values.gcr.projectId }}"
- name: PUBSUB
value: "1"
value: "true"
{{- if .Values.gcr.clusterName }}
# Customize the cluster name, mainly useful when outside of GKE
- name: CLUSTER_NAME

View File

@ -18,6 +18,8 @@ polling:
# Helm provider support
helmProvider:
enabled: true
# set to version "v3" for Helm v3
version: "v2"
tillerNamespace: "kube-system"
# optional Tiller address (if portforwarder tunnel doesn't work),
# if you are using default configuration, setting it to
@ -126,17 +128,17 @@ secret:
# Keel self-update
# uncomment lines below if you want Keel to automaticly
# self-update to the latest release version
keel:
# keel policy (all/major/minor/patch/force)
policy: patch
# trigger type, defaults to events such as pubsub, webhooks
trigger: poll
# polling schedule
pollSchedule: "@every 3m"
# images to track and update
images:
- repository: image.repository
tag: image.tag
# keel:
# # keel policy (all/major/minor/patch/force)
# policy: patch
# # trigger type, defaults to events such as pubsub, webhooks
# trigger: poll
# # polling schedule
# pollSchedule: "@every 3m"
# # images to track and update
# images:
# - repository: image.repository
# tag: image.tag
# RBAC manifests management
rbac:

View File

@ -10,7 +10,6 @@ import (
"context"
"github.com/prometheus/client_golang/prometheus"
netContext "golang.org/x/net/context"
kingpin "gopkg.in/alecthomas/kingpin.v2"
kube "k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
@ -32,6 +31,7 @@ import (
"github.com/keel-hq/keel/internal/workgroup"
"github.com/keel-hq/keel/provider"
"github.com/keel-hq/keel/provider/helm"
"github.com/keel-hq/keel/provider/helm3"
"github.com/keel-hq/keel/provider/kubernetes"
"github.com/keel-hq/keel/registry"
"github.com/keel-hq/keel/secrets"
@ -58,6 +58,9 @@ import (
_ "github.com/keel-hq/keel/bot/slack"
log "github.com/sirupsen/logrus"
// importing to ensure correct dependencies
_ "helm.sh/helm/v3/pkg/action"
)
// gcloud pubsub related config
@ -70,6 +73,7 @@ const (
EnvHelmProvider = "HELM_PROVIDER" // helm provider
EnvHelmTillerAddress = "TILLER_ADDRESS" // helm provider
EnvHelmTillerNamespace = "TILLER_NAMESPACE" // helm provider
EnvHelm3Provider = "HELM3_PROVIDER" // helm3 provider
EnvUIDir = "UI_DIR"
// EnvDefaultDockerRegistryCfg - default registry configuration that can be passed into
@ -134,7 +138,7 @@ func main() {
notification.RegisterSender("auditor", auditLogger)
// setting up triggers
ctx, cancel := netContext.WithCancel(context.Background())
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
notificationLevel := types.LevelInfo
@ -355,6 +359,23 @@ func setupProviders(opts *ProviderOpts) (providers provider.Providers) {
enabledProviders = append(enabledProviders, helmProvider)
}
if os.Getenv(EnvHelm3Provider) == "1" || os.Getenv(EnvHelm3Provider) == "true" {
helm3Implementer := helm3.NewHelm3Implementer()
helm3Provider := helm3.NewProvider(helm3Implementer, opts.sender, opts.approvalsManager)
go func() {
err := helm3Provider.Start()
if err != nil {
log.WithFields(log.Fields{
"error": err,
}).Fatal("helm3 provider stopped with an error")
}
}()
enabledProviders = append(enabledProviders, helm3Provider)
}
providers = provider.New(enabledProviders, opts.approvalsManager)
return providers
@ -426,7 +447,7 @@ func setupTriggers(ctx context.Context, opts *TriggerOpts) (teardown func()) {
go subManager.Start(ctx)
}
if os.Getenv(EnvTriggerPoll) != "0" {
if os.Getenv(EnvTriggerPoll) != "0" || os.Getenv(EnvTriggerPoll) != "false" {
registryClient := registry.New()
watcher := poll.NewRepositoryWatcher(opts.providers, registryClient)

View File

@ -92,11 +92,11 @@ func (s *sender) Send(event types.EventNotification) error {
params.IconURL = constants.KeelLogoURL
attachements := []slack.Attachment{
slack.Attachment{
{
Fallback: event.Message,
Color: event.Level.Color(),
Fields: []slack.AttachmentField{
slack.AttachmentField{
{
Title: event.Type.String(),
Value: event.Message,
Short: false,

184
go.mod
View File

@ -2,119 +2,73 @@ module github.com/keel-hq/keel
go 1.14
require (
cloud.google.com/go v0.40.0
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78
github.com/BurntSushi/toml v0.3.1
github.com/MakeNowJust/heredoc v0.0.0-20171113091838-e9091a26100e
github.com/Masterminds/goutils v1.1.0
github.com/Masterminds/semver v1.4.2
github.com/Masterminds/sprig v2.19.0+incompatible
github.com/PuerkitoBio/purell v1.1.1
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf
github.com/aws/aws-sdk-go v1.20.0
github.com/beorn7/perks v1.0.0
github.com/chai2010/gettext-go v0.0.0-20170215093142-bf70f2a70fb1
github.com/cyphar/filepath-securejoin v0.2.2
github.com/daneharrigan/hipchat v0.0.0-20170512185232-835dc879394a
github.com/davecgh/go-spew v1.1.1
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/docker/distribution v2.6.0-rc.1.0.20180227233429-6fca8d6e6713+incompatible
github.com/docker/docker v17.12.0-ce-rc1.0.20180612054059-a9fbbdc8dd87+incompatible
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7
github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c
github.com/emicklei/go-restful v2.9.6+incompatible
github.com/evanphx/json-patch v3.0.0+incompatible
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d
github.com/ghodss/yaml v1.0.0
github.com/go-openapi/jsonpointer v0.19.0
github.com/go-openapi/jsonreference v0.19.0
github.com/go-openapi/spec v0.19.0
github.com/go-openapi/swag v0.19.0
github.com/gobwas/glob v0.2.3
github.com/gogo/protobuf v1.2.1
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef
github.com/golang/protobuf v1.3.1
github.com/google/btree v1.0.0
github.com/google/go-querystring v1.0.0
github.com/google/gofuzz v1.0.0
github.com/google/uuid v1.1.1
github.com/googleapis/gnostic v0.2.0
github.com/gorilla/mux v1.7.2
github.com/gorilla/websocket v1.4.0
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79
github.com/hashicorp/golang-lru v0.5.1
github.com/huandu/xstrings v1.2.0
github.com/imdario/mergo v0.3.7
github.com/inconshreveable/mousetrap v1.0.0
github.com/jinzhu/gorm v1.9.9
github.com/jinzhu/inflection v1.0.0
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af
github.com/jmoiron/sqlx v1.2.0
github.com/json-iterator/go v1.1.6
github.com/konsorten/go-windows-terminal-sequences v1.0.2
github.com/lib/pq v1.1.1
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983
github.com/mattn/go-sqlite3 v1.10.0
github.com/matttproud/golang_protobuf_extensions v1.0.1
github.com/mfridman/tparse v0.7.4 // indirect
github.com/mitchellh/go-wordwrap v1.0.0
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd
github.com/modern-go/reflect2 v1.0.1
github.com/nlopes/slack v0.5.0
github.com/opencontainers/go-digest v1.0.0-rc1
github.com/petar/GoLLRB v0.0.0-20190514000832-33fb24c13b99
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pkg/errors v0.8.1
github.com/prometheus/client_golang v0.9.4
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90
github.com/prometheus/common v0.4.1
github.com/prometheus/procfs v0.0.2
github.com/rubenv/sql-migrate v0.0.0-20190327083759-54bad0a9b051
github.com/rusenask/cron v1.1.0
github.com/rusenask/docker-registry-client v0.0.0-20190426095143-dbc590492100
github.com/russross/blackfriday v1.5.2
github.com/ryanuber/go-glob v0.0.0-20160226084822-572520ed46db
github.com/shurcooL/sanitized_anchor_name v1.0.0
github.com/sirupsen/logrus v1.4.2
github.com/spf13/cobra v0.0.5
github.com/spf13/pflag v1.0.3
github.com/stretchr/testify v1.3.0
github.com/tbruyelle/hipchat-go v0.0.0-20160921153256-749fb9e14beb
github.com/urfave/negroni v0.3.0
go.opencensus.io v0.22.0
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
golang.org/x/sync v0.0.0-20190423024810-112230192c58
golang.org/x/sys v0.0.0-20190613124609-5ed2794edfdc
golang.org/x/text v0.3.2
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4
google.golang.org/api v0.6.0
google.golang.org/appengine v1.6.1
google.golang.org/genproto v0.0.0-20190611190212-a7e196e89fd3
google.golang.org/grpc v1.21.1
gopkg.in/alecthomas/kingpin.v2 v2.2.6
gopkg.in/gorp.v1 v1.7.2
gopkg.in/inf.v0 v0.9.1
gopkg.in/square/go-jose.v2 v2.3.1
gopkg.in/yaml.v2 v2.2.2
k8s.io/api v0.0.0-20190516230258-a675ac48af67
k8s.io/apiextensions-apiserver v0.0.0-20190516231611-bf6753f2aa24
k8s.io/apimachinery v0.0.0-20190404173353-6a84e37a896d
k8s.io/apiserver v0.0.0-20190516230822-f89599b3f645
k8s.io/cli-runtime v0.0.0-20190516231937-17bc0b7fcef5
k8s.io/client-go v11.0.1-0.20190516230509-ae8359b20417+incompatible
k8s.io/cloud-provider v0.0.0-20190323031113-9c9d72d1bf90
k8s.io/helm v2.14.1+incompatible
k8s.io/klog v0.3.3
k8s.io/kube-openapi v0.0.0-20190603182131-db7b694dc208
k8s.io/kubernetes v1.14.2
k8s.io/utils v0.0.0-20190607212802-c55fbcfc754a
sigs.k8s.io/kustomize v2.0.3+incompatible
sigs.k8s.io/yaml v1.1.0
vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787
replace (
k8s.io/api => k8s.io/api v0.16.10
k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.16.10
k8s.io/apimachinery => k8s.io/apimachinery v0.16.10
k8s.io/apiserver => k8s.io/apiserver v0.16.10
k8s.io/cli-runtime => k8s.io/cli-runtime v0.16.10
k8s.io/client-go => k8s.io/client-go v0.16.10
k8s.io/cloud-provider => k8s.io/cloud-provider v0.16.10
k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.16.10
k8s.io/code-generator => k8s.io/code-generator v0.16.10
k8s.io/component-base => k8s.io/component-base v0.16.10
k8s.io/cri-api => k8s.io/cri-api v0.16.10
k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.16.10
k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.16.10
k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.16.10
k8s.io/kube-proxy => k8s.io/kube-proxy v0.16.10
k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.16.10
k8s.io/kubectl => k8s.io/kubectl v0.16.10
k8s.io/kubelet => k8s.io/kubelet v0.16.10
k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.16.10
k8s.io/metrics => k8s.io/metrics v0.16.10
k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.16.10
)
replace (
helm.sh/helm/v3 => helm.sh/helm/v3 v3.1.2
k8s.io/helm => k8s.io/helm v2.16.7+incompatible
)
replace k8s.io/kubernetes => k8s.io/kubernetes v1.16.10
require (
cloud.google.com/go/pubsub v1.4.0
github.com/Masterminds/semver v1.5.0
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
github.com/aws/aws-sdk-go v1.31.10
github.com/daneharrigan/hipchat v0.0.0-20170512185232-835dc879394a
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/docker/distribution v2.7.1+incompatible
github.com/ghodss/yaml v1.0.0
github.com/golang/protobuf v1.4.2
github.com/google/uuid v1.1.1
github.com/gorilla/mux v1.7.4
github.com/jinzhu/gorm v1.9.12
github.com/jmoiron/sqlx v1.2.0 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/mfridman/tparse v0.7.4 // indirect
github.com/nlopes/slack v0.6.0
github.com/opencontainers/go-digest v1.0.0
github.com/prometheus/client_golang v1.6.0
github.com/rubenv/sql-migrate v0.0.0-20200429072036-ae26b214fa43 // indirect
github.com/rusenask/cron v1.1.0
github.com/rusenask/docker-registry-client v0.0.0-20200210164146-049272422097
github.com/ryanuber/go-glob v1.0.0
github.com/sirupsen/logrus v1.6.0
github.com/stretchr/testify v1.5.1
github.com/tbruyelle/hipchat-go v0.0.0-20170717082847-35aebc99209a
github.com/urfave/negroni v1.0.0
golang.org/x/net v0.0.0-20200602114024-627f9648deb9
google.golang.org/api v0.26.0
google.golang.org/grpc v1.29.1
gopkg.in/alecthomas/kingpin.v2 v2.2.6
helm.sh/helm/v3 v3.0.0-00010101000000-000000000000
k8s.io/api v0.17.2
k8s.io/apimachinery v0.17.2
k8s.io/cli-runtime v0.17.2
k8s.io/client-go v0.17.2
k8s.io/helm v0.0.0-00010101000000-000000000000
sigs.k8s.io/yaml v1.1.0
)

1304
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -101,6 +101,7 @@ func (s *TriggerServer) Start() error {
log.WithFields(log.Fields{
"port": s.port,
}).Info("webhook trigger server starting...")
return s.server.ListenAndServe()
}

View File

@ -10,9 +10,9 @@ import (
log "github.com/sirupsen/logrus"
)
// namespace/release name/version
func getIdentifier(namespace, name, version string) string {
return namespace + "/" + name + ":" + version
// namespace/release name:version
func getIdentifier(plan *UpdatePlan) string {
return fmt.Sprintf("%s/%s:%s", plan.Namespace, plan.Name, plan.NewVersion)
}
func (p *Provider) checkForApprovals(event *types.Event, plans []*UpdatePlan) (approvedPlans []*UpdatePlan) {
@ -24,6 +24,7 @@ func (p *Provider) checkForApprovals(event *types.Event, plans []*UpdatePlan) (a
"error": err,
"release_name": plan.Name,
"namespace": plan.Namespace,
"version": plan.NewVersion,
}).Error("provider.helm: failed to check approval status for deployment")
continue
}
@ -36,7 +37,7 @@ func (p *Provider) checkForApprovals(event *types.Event, plans []*UpdatePlan) (a
// updateComplete is called after we successfully update resource
func (p *Provider) updateComplete(plan *UpdatePlan) error {
return p.approvalManager.Archive(getIdentifier(plan.Namespace, plan.Name, plan.NewVersion))
return p.approvalManager.Archive(getIdentifier(plan))
}
func (p *Provider) isApproved(event *types.Event, plan *UpdatePlan) (bool, error) {
@ -44,7 +45,7 @@ func (p *Provider) isApproved(event *types.Event, plan *UpdatePlan) (bool, error
return true, nil
}
identifier := getIdentifier(plan.Namespace, plan.Name, plan.NewVersion)
identifier := getIdentifier(plan)
// checking for existing approval
existing, err := p.approvalManager.Get(identifier)

File diff suppressed because it is too large Load Diff

View File

@ -181,6 +181,10 @@ func (p *Provider) TrackedImages() ([]*types.TrackedImage, error) {
for _, release := range releases {
// getting configuration
if release.Chart.Metadata.Name == "" {
return nil, err
}
vals, err := values(release.Chart, release.Config)
if err != nil {
log.WithFields(log.Fields{
@ -344,7 +348,7 @@ func (p *Provider) applyPlans(plans []*UpdatePlan) error {
"error": err,
"name": plan.Name,
"namespace": plan.Namespace,
}).Warn("provider.helm: got error while resetting approvals counter after successful update")
}).Debug("provider.helm: got error while resetting approvals counter after successful update")
}
var msg string

View File

@ -113,13 +113,12 @@ keel:
images:
- repository: image.repository
tag: image.tag
`
fakeImpl := &fakeImplementer{
listReleasesResponse: &rls.ListReleasesResponse{
Releases: []*hapi_release5.Release{
&hapi_release5.Release{
{
Name: "release-1",
Chart: &chart.Chart{
Values: &chart.Config{Raw: chartVals},
@ -165,7 +164,7 @@ func TestGetChartPolicyFromProm(t *testing.T) {
fakeImpl := &fakeImplementer{
listReleasesResponse: &rls.ListReleasesResponse{
Releases: []*hapi_release5.Release{
&hapi_release5.Release{
{
Name: "release-1",
Chart: &chart.Chart{
Values: &chart.Config{Raw: promChartValues},
@ -233,7 +232,7 @@ keel:
fakeImpl := &fakeImplementer{
listReleasesResponse: &rls.ListReleasesResponse{
Releases: []*hapi_release5.Release{
&hapi_release5.Release{
{
Name: "release-1",
Chart: &chart.Chart{
Values: &chart.Config{Raw: chartVals},
@ -276,7 +275,7 @@ image2:
fakeImpl := &fakeImplementer{
listReleasesResponse: &rls.ListReleasesResponse{
Releases: []*hapi_release5.Release{
&hapi_release5.Release{
{
Name: "release-1",
Chart: &chart.Chart{
Values: &chart.Config{Raw: chartVals},
@ -327,7 +326,7 @@ keel:
fakeImpl := &fakeImplementer{
listReleasesResponse: &rls.ListReleasesResponse{
Releases: []*hapi_release5.Release{
&hapi_release5.Release{
{
Name: "release-1",
Chart: &chart.Chart{
Values: &chart.Config{Raw: chartVals},
@ -385,7 +384,7 @@ func TestGetPolicyFromConfig(t *testing.T) {
func TestGetImagesFromConfig(t *testing.T) {
vals, err := testingConfigYaml(&KeelChartConfig{Policy: "all", Images: []ImageDetails{
ImageDetails{
{
RepositoryPath: "repopath",
TagPath: "tagpath",
},
@ -428,13 +427,14 @@ keel:
`
myChart := &chart.Chart{
Values: &chart.Config{Raw: chartVals},
Values: &chart.Config{Raw: chartVals},
Metadata: &chart.Metadata{Name: "app-x"},
}
fakeImpl := &fakeImplementer{
listReleasesResponse: &rls.ListReleasesResponse{
Releases: []*hapi_release5.Release{
&hapi_release5.Release{
{
Name: "release-1",
Chart: myChart,
Config: &chart.Config{Raw: ""},
@ -596,7 +596,7 @@ keel:
MatchPreRelease: true,
Trigger: types.TriggerTypeDefault,
Images: []ImageDetails{
ImageDetails{RepositoryPath: "image.repository", TagPath: "image.tag"},
{RepositoryPath: "image.repository", TagPath: "image.tag"},
},
Plc: policy.NewSemverPolicy(policy.SemverPolicyTypeAll, true),
},
@ -610,7 +610,7 @@ keel:
Trigger: types.TriggerTypeDefault,
NotificationChannels: []string{"chan1", "chan2"},
Images: []ImageDetails{
ImageDetails{RepositoryPath: "image.repository", TagPath: "image.tag"},
{RepositoryPath: "image.repository", TagPath: "image.tag"},
},
Plc: policy.NewSemverPolicy(policy.SemverPolicyTypeAll, true),
},
@ -624,7 +624,7 @@ keel:
Trigger: types.TriggerTypePoll,
PollSchedule: "@every 30m",
Images: []ImageDetails{
ImageDetails{RepositoryPath: "image.repository", TagPath: "image.tag", ImagePullSecret: "such-secret"},
{RepositoryPath: "image.repository", TagPath: "image.tag", ImagePullSecret: "such-secret"},
},
Plc: policy.NewSemverPolicy(policy.SemverPolicyTypeMajor, true),
},
@ -637,7 +637,7 @@ keel:
MatchPreRelease: false,
Trigger: types.TriggerTypeDefault,
Images: []ImageDetails{
ImageDetails{RepositoryPath: "image.repository", TagPath: "image.tag"},
{RepositoryPath: "image.repository", TagPath: "image.tag"},
},
Plc: policy.NewSemverPolicy(policy.SemverPolicyTypeAll, false),
},
@ -681,7 +681,7 @@ keel:
fakeImpl := &fakeImplementer{
listReleasesResponse: &rls.ListReleasesResponse{
Releases: []*hapi_release5.Release{
&hapi_release5.Release{
{
Name: "release-1",
Chart: &chart.Chart{
Values: &chart.Config{Raw: chartVals},

View File

@ -65,15 +65,18 @@ keel:
`
helloWorldChart := &hapi_chart.Chart{
Values: &hapi_chart.Config{Raw: chartValuesPolicyForce},
Values: &hapi_chart.Config{Raw: chartValuesPolicyForce},
Metadata: &hapi_chart.Metadata{Name: "app-x"},
}
helloWorldChartPolicyMajor := &hapi_chart.Chart{
Values: &hapi_chart.Config{Raw: chartValuesPolicyMajor},
Values: &hapi_chart.Config{Raw: chartValuesPolicyMajor},
Metadata: &hapi_chart.Metadata{Name: "app-x"},
}
helloWorldChartPolicyMajorReleaseNotes := &hapi_chart.Chart{
Values: &hapi_chart.Config{Raw: chartValuesPolicyForceReleaseNotes},
Values: &hapi_chart.Config{Raw: chartValuesPolicyForceReleaseNotes},
Metadata: &hapi_chart.Metadata{Name: "app-x"},
}
type args struct {
@ -111,7 +114,7 @@ keel:
MatchPreRelease: true,
Trigger: types.TriggerTypePoll,
Images: []ImageDetails{
ImageDetails{
{
RepositoryPath: "image.repository",
TagPath: "image.tag",
},
@ -144,7 +147,7 @@ keel:
MatchPreRelease: true,
Trigger: types.TriggerTypePoll,
Images: []ImageDetails{
ImageDetails{
{
RepositoryPath: "image.repository",
TagPath: "image.tag",
ReleaseNotes: "https://github.com/keel-hq/keel/releases",
@ -271,21 +274,26 @@ image:
`
helloWorldChart := &hapi_chart.Chart{
Values: &hapi_chart.Config{Raw: chartValuesA},
Values: &hapi_chart.Config{Raw: chartValuesA},
Metadata: &hapi_chart.Metadata{Name: "app-x"},
}
helloWorldNonSemverChart := &hapi_chart.Chart{
Values: &hapi_chart.Config{Raw: chartValuesB},
Values: &hapi_chart.Config{Raw: chartValuesB},
Metadata: &hapi_chart.Metadata{Name: "app-x"},
}
helloWorldNonSemverNoForceChart := &hapi_chart.Chart{
Values: &hapi_chart.Config{Raw: chartValuesNonSemverNoForce},
Values: &hapi_chart.Config{Raw: chartValuesNonSemverNoForce},
Metadata: &hapi_chart.Metadata{Name: "app-x"},
}
helloWorldNoTagChart := &hapi_chart.Chart{
Values: &hapi_chart.Config{Raw: chartValuesNoTag},
Values: &hapi_chart.Config{Raw: chartValuesNoTag},
Metadata: &hapi_chart.Metadata{Name: "app-x"},
}
helloWorldNoKeelCfg := &hapi_chart.Chart{
Values: &hapi_chart.Config{Raw: chartValuesNoKeelCfg},
Values: &hapi_chart.Config{Raw: chartValuesNoKeelCfg},
Metadata: &hapi_chart.Metadata{Name: "app-x"},
}
type args struct {
@ -324,7 +332,7 @@ image:
MatchPreRelease: true,
Trigger: types.TriggerTypePoll,
Images: []ImageDetails{
ImageDetails{RepositoryPath: "image.repository", TagPath: "image.tag"},
{RepositoryPath: "image.repository", TagPath: "image.tag"},
},
Plc: policy.NewSemverPolicy(policy.SemverPolicyTypeAll, true),
},
@ -381,7 +389,7 @@ image:
MatchPreRelease: true,
Trigger: types.TriggerTypePoll,
Images: []ImageDetails{
ImageDetails{RepositoryPath: "image.repository", TagPath: "image.tag"},
{RepositoryPath: "image.repository", TagPath: "image.tag"},
},
Plc: policy.NewForcePolicy(false),
},
@ -425,7 +433,7 @@ image:
MatchPreRelease: true,
Trigger: types.TriggerTypePoll,
Images: []ImageDetails{
ImageDetails{RepositoryPath: "image.repository"},
{RepositoryPath: "image.repository"},
},
Plc: policy.NewSemverPolicy(policy.SemverPolicyTypeMajor, true),
},

92
provider/helm3/approvals.go Executable file
View File

@ -0,0 +1,92 @@
package helm3
import (
"fmt"
"time"
"github.com/keel-hq/keel/pkg/store"
"github.com/keel-hq/keel/types"
log "github.com/sirupsen/logrus"
)
// namespace/release name:version
func getIdentifier(plan *UpdatePlan) string {
return fmt.Sprintf("%s/%s:%s", plan.Namespace, plan.Name, plan.NewVersion)
}
func (p *Provider) checkForApprovals(event *types.Event, plans []*UpdatePlan) (approvedPlans []*UpdatePlan) {
approvedPlans = []*UpdatePlan{}
for _, plan := range plans {
approved, err := p.isApproved(event, plan)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"release_name": plan.Name,
"namespace": plan.Namespace,
"version": plan.NewVersion,
}).Error("provider.helm3: failed to check approval status for deployment")
continue
}
if approved {
approvedPlans = append(approvedPlans, plan)
}
}
return approvedPlans
}
// updateComplete is called after we successfully update resource
func (p *Provider) updateComplete(plan *UpdatePlan) error {
return p.approvalManager.Archive(getIdentifier(plan))
}
func (p *Provider) isApproved(event *types.Event, plan *UpdatePlan) (bool, error) {
if plan.Config.Approvals == 0 {
return true, nil
}
identifier := getIdentifier(plan)
// checking for existing approval
existing, err := p.approvalManager.Get(identifier)
if err != nil {
if err == store.ErrRecordNotFound {
// if approval doesn't exist and trigger wasn't existing approval fulfillment -
// create a new one, otherwise if several deployments rely on the same image, it would just be
// requesting approvals in a loop
if event.TriggerName == types.TriggerTypeApproval.String() {
return false, nil
}
if plan.Config.ApprovalDeadline == 0 {
plan.Config.ApprovalDeadline = types.KeelApprovalDeadlineDefault
}
// creating new one
approval := &types.Approval{
Provider: types.ProviderTypeHelm,
Identifier: identifier,
Event: event,
CurrentVersion: plan.CurrentVersion,
NewVersion: plan.NewVersion,
VotesRequired: plan.Config.Approvals,
VotesReceived: 0,
Rejected: false,
Deadline: time.Now().Add(time.Duration(plan.Config.ApprovalDeadline) * time.Hour),
}
approval.Message = fmt.Sprintf("New image is available for release %s/%s (%s).",
plan.Namespace,
plan.Name,
approval.Delta(),
)
return false, p.approvalManager.Create(approval)
}
return false, err
}
return existing.Status() == types.ApprovalStatusApproved, nil
}

105
provider/helm3/common.go Normal file
View File

@ -0,0 +1,105 @@
package helm3
import (
"errors"
"fmt"
"github.com/keel-hq/keel/types"
"github.com/keel-hq/keel/util/image"
"helm.sh/helm/v3/pkg/chartutil"
log "github.com/sirupsen/logrus"
)
// ErrKeelConfigNotFound - default error when keel configuration for chart is not defined
var ErrKeelConfigNotFound = errors.New("keel configuration not found")
// getImages - get images from chart values
func getImages(vals chartutil.Values) ([]*types.TrackedImage, error) {
var images []*types.TrackedImage
keelCfg, err := getKeelConfig(vals)
if err != nil {
if err == ErrPolicyNotSpecified {
// nothing to do
return images, nil
}
log.WithFields(log.Fields{
"error": err,
}).Error("provider.helm3: failed to get keel configuration for release")
// ignoring this release, no keel config found
return nil, ErrKeelConfigNotFound
}
for _, imageDetails := range keelCfg.Images {
imageRef, err := parseImage(vals, &imageDetails)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"repository_name": imageDetails.RepositoryPath,
"repository_tag": imageDetails.TagPath,
}).Error("provider.helm3: failed to parse image")
continue
}
trackedImage := &types.TrackedImage{
Image: imageRef,
PollSchedule: keelCfg.PollSchedule,
Trigger: keelCfg.Trigger,
Policy: keelCfg.Plc,
}
if imageDetails.ImagePullSecret != "" {
trackedImage.Secrets = append(trackedImage.Secrets, imageDetails.ImagePullSecret)
}
images = append(images, trackedImage)
}
return images, nil
}
func getPlanValues(newVersion *types.Version, ref *image.Reference, imageDetails *ImageDetails) (path, value string) {
// if tag is not supplied, then user specified full image name
if imageDetails.TagPath == "" {
return imageDetails.RepositoryPath, getUpdatedImage(ref, newVersion.String())
}
return imageDetails.TagPath, newVersion.String()
}
func getUnversionedPlanValues(newTag string, ref *image.Reference, imageDetails *ImageDetails) (path, value string) {
// if tag is not supplied, then user specified full image name
if imageDetails.TagPath == "" {
return imageDetails.RepositoryPath, getUpdatedImage(ref, newTag)
}
return imageDetails.TagPath, newTag
}
func getUpdatedImage(ref *image.Reference, version string) string {
// updating image
if ref.Registry() == image.DefaultRegistryHostname {
return fmt.Sprintf("%s:%s", ref.ShortName(), version)
}
return fmt.Sprintf("%s:%s", ref.Repository(), version)
}
func parseImage(vals chartutil.Values, details *ImageDetails) (*image.Reference, error) {
if details.RepositoryPath == "" {
return nil, fmt.Errorf("repository name path cannot be empty")
}
imageName, err := getValueAsString(vals, details.RepositoryPath)
if err != nil {
return nil, err
}
// getting image tag
imageTag, err := getValueAsString(vals, details.TagPath)
if err != nil {
// failed to find tag, returning anyway
return image.Parse(imageName)
}
return image.Parse(imageName + ":" + imageTag)
}

View File

@ -0,0 +1,144 @@
package helm3
import (
"reflect"
"testing"
"github.com/keel-hq/keel/internal/policy"
"github.com/keel-hq/keel/types"
"github.com/keel-hq/keel/util/image"
"helm.sh/helm/v3/pkg/chartutil"
)
var chartValuesA = `
name: al Rashid
where:
city: Basrah
title: caliph
image:
repository: gcr.io/v2-namespace/hello-world
tag: 1.1.0
keel:
policy: all
trigger: poll
images:
- repository: image.repository
tag: image.tag
`
func mustParse(name string) *image.Reference {
img, err := image.Parse(name)
if err != nil {
panic(err)
}
return img
}
func Test_getImages(t *testing.T) {
vals, _ := chartutil.ReadValues([]byte(chartValuesA))
img, _ := image.Parse("gcr.io/v2-namespace/hello-world:1.1.0")
promVals, _ := chartutil.ReadValues([]byte(promChartValues))
type args struct {
vals chartutil.Values
}
tests := []struct {
name string
args args
want []*types.TrackedImage
wantErr bool
}{
{
name: "hello-world image",
args: args{
vals: vals,
},
want: []*types.TrackedImage{
{
Image: img,
Trigger: types.TriggerTypePoll,
Policy: policy.NewSemverPolicy(policy.SemverPolicyTypeAll, true),
},
},
wantErr: false,
},
{
name: "prom config from https://raw.githubusercontent.com/helm/charts/master/stable/prometheus-operator/values.yaml",
args: args{
vals: promVals,
},
want: []*types.TrackedImage{
{
Image: mustParse("quay.io/prometheus/alertmanager:v0.16.2"),
Trigger: types.TriggerTypePoll,
Policy: policy.NewSemverPolicy(policy.SemverPolicyTypeAll, true),
},
{
Image: mustParse("quay.io/coreos/prometheus-operator:v0.29.0"),
Trigger: types.TriggerTypePoll,
Policy: policy.NewSemverPolicy(policy.SemverPolicyTypeAll, true),
},
{
Image: mustParse("quay.io/prometheus/prometheus:v2.7.2"),
Trigger: types.TriggerTypePoll,
Policy: policy.NewSemverPolicy(policy.SemverPolicyTypeAll, true),
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := getImages(tt.args.vals)
if (err != nil) != tt.wantErr {
t.Errorf("getImages() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("getImages() = %v, want %v", got, tt.want)
}
})
}
}
var promChartValues = `# Default values for prometheus-operator.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
# keel config
keel:
policy: all
trigger: poll
images:
- repository: alertmanager.alertmanagerSpec.image.repository
tag: alertmanager.alertmanagerSpec.image.tag
- repository: prometheusOperator.image.repository
tag: prometheusOperator.image.tag
- repository: prometheus.prometheusSpec.image.repository
tag: prometheus.prometheusSpec.image.tag
alertmanager:
enabled: true
alertmanagerSpec:
image:
repository: quay.io/prometheus/alertmanager
tag: v0.16.2
prometheusOperator:
enabled: true
image:
repository: quay.io/coreos/prometheus-operator
tag: v0.29.0
pullPolicy: IfNotPresent
prometheus:
enabled: true
prometheusSpec:
image:
repository: quay.io/prometheus/prometheus
tag: v2.7.2
`

462
provider/helm3/helm3.go Normal file
View File

@ -0,0 +1,462 @@
package helm3
import (
"errors"
"fmt"
"strings"
"time"
"github.com/keel-hq/keel/approvals"
"github.com/keel-hq/keel/internal/policy"
"github.com/keel-hq/keel/types"
"github.com/keel-hq/keel/util/image"
"github.com/prometheus/client_golang/prometheus"
"github.com/keel-hq/keel/extension/notification"
log "github.com/sirupsen/logrus"
"sigs.k8s.io/yaml"
"helm.sh/helm/v3/pkg/strvals"
_ "helm.sh/helm/v3/pkg/action"
_ "helm.sh/helm/v3/pkg/release"
hapi_chart "helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chartutil"
)
var helm3VersionedUpdatesCounter = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "helm3_versioned_updates_total",
Help: "How many versioned helm3 charts were updated, partitioned by chart name.",
},
[]string{"chart"},
)
var helm3UnversionedUpdatesCounter = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "helm3_unversioned_updates_total",
Help: "How many unversioned helm3 charts were updated, partitioned by chart name.",
},
[]string{"chart"},
)
func init() {
prometheus.MustRegister(helm3VersionedUpdatesCounter)
prometheus.MustRegister(helm3UnversionedUpdatesCounter)
}
// ErrPolicyNotSpecified helm related errors
var (
ErrPolicyNotSpecified = errors.New("policy not specified")
)
// Manager - high level interface into helm provider related data used by
// triggers
type Manager interface {
Images() ([]*image.Reference, error)
}
// ProviderName - helm provider name
const ProviderName = "helm3"
// DefaultUpdateTimeout - update timeout in seconds
// const DefaultUpdateTimeout = 300
// UpdatePlan - release update plan
type UpdatePlan struct {
Namespace string
Name string
Config *KeelChartConfig
// chart
Chart *hapi_chart.Chart
// values to update path=value
Values map[string]string
// Current (last seen cluster version)
CurrentVersion string
// New version that's already in the deployment
NewVersion string
// ReleaseNotes is a slice of combined release notes.
ReleaseNotes []string
// used as fix to bug in chartutil.coalesce v3.1.2
EmptyConfig bool
}
// keel:
// # keel policy (all/major/minor/patch/force)
// policy: all
// # trigger type, defaults to events such as pubsub, webhooks
// trigger: poll
// pollSchedule: "@every 2m"
// # images to track and update
// images:
// - repository: image.repository
// tag: image.tag
// Root - root element of the values yaml
type Root struct {
Keel KeelChartConfig `json:"keel"`
}
// KeelChartConfig - keel related configuration taken from values.yaml
type KeelChartConfig struct {
Policy string `json:"policy"`
MatchTag bool `json:"matchTag"`
MatchPreRelease bool `json:"matchPreRelease"`
Trigger types.TriggerType `json:"trigger"`
PollSchedule string `json:"pollSchedule"`
Approvals int `json:"approvals"` // Minimum required approvals
ApprovalDeadline int `json:"approvalDeadline"` // Deadline in hours
Images []ImageDetails `json:"images"`
NotificationChannels []string `json:"notificationChannels"` // optional notification channels
Plc policy.Policy `json:"-"`
}
// ImageDetails - image details
type ImageDetails struct {
RepositoryPath string `json:"repository"`
TagPath string `json:"tag"`
DigestPath string `json:"digest"`
ReleaseNotes string `json:"releaseNotes"`
ImagePullSecret string `json:"imagePullSecret"`
}
// Provider - helm3 provider, responsible for managing release updates
type Provider struct {
implementer Implementer
sender notification.Sender
approvalManager approvals.Manager
events chan *types.Event
stop chan struct{}
}
// NewProvider - create new Helm provider
func NewProvider(implementer Implementer, sender notification.Sender, approvalManager approvals.Manager) *Provider {
return &Provider{
implementer: implementer,
approvalManager: approvalManager,
sender: sender,
events: make(chan *types.Event, 100),
stop: make(chan struct{}),
}
}
// GetName - get provider name
func (p *Provider) GetName() string {
return ProviderName
}
// Submit - submit event to provider
func (p *Provider) Submit(event types.Event) error {
p.events <- &event
return nil
}
// Start - starts kubernetes provider, waits for events
func (p *Provider) Start() error {
return p.startInternal()
}
// Stop - stops kubernetes provider
func (p *Provider) Stop() {
close(p.stop)
}
// TrackedImages - returns tracked images from all releases that have keel configuration
func (p *Provider) TrackedImages() ([]*types.TrackedImage, error) {
var trackedImages []*types.TrackedImage
releases, err := p.implementer.ListReleases()
if err != nil {
return nil, err
}
for _, release := range releases {
// getting configuration
vals, err := values(release.Chart, release.Config)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"release": release.Name,
"namespace": release.Namespace,
}).Error("provider.helm3: failed to get values.yaml for release")
continue
}
cfg, err := getKeelConfig(vals)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"release": release.Name,
"namespace": release.Namespace,
}).Debug("provider.helm3: failed to get config for release")
continue
}
if cfg.PollSchedule == "" {
cfg.PollSchedule = types.KeelPollDefaultSchedule
}
// used to check pod secrets
selector := fmt.Sprintf("app=%s,release=%s", release.Chart.Metadata.Name, release.Name)
releaseImages, err := getImages(vals)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"release": release.Name,
"namespace": release.Namespace,
}).Error("provider.helm3: failed to get images for release")
continue
}
for _, img := range releaseImages {
img.Meta = map[string]string{
"selector": selector,
"helm.sh/chart": fmt.Sprintf("%s-%s", release.Chart.Metadata.Name, release.Chart.Metadata.Version),
}
img.Namespace = release.Namespace
img.Provider = ProviderName
trackedImages = append(trackedImages, img)
}
}
return trackedImages, nil
}
func (p *Provider) startInternal() error {
for {
select {
case event := <-p.events:
err := p.processEvent(event)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"image": event.Repository.Name,
"tag": event.Repository.Tag,
}).Error("provider.helm3: failed to process event")
}
case <-p.stop:
log.Info("provider.helm3: got shutdown signal, stopping...")
return nil
}
}
}
func (p *Provider) processEvent(event *types.Event) (err error) {
plans, err := p.createUpdatePlans(event)
if err != nil {
return err
}
approved := p.checkForApprovals(event, plans)
return p.applyPlans(approved)
}
func (p *Provider) createUpdatePlans(event *types.Event) ([]*UpdatePlan, error) {
var plans []*UpdatePlan
releases, err := p.implementer.ListReleases()
if err != nil {
return nil, err
}
for _, release := range releases {
plan, update, err := checkRelease(&event.Repository, release.Namespace, release.Name, release.Chart, release.Config)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"name": release.Name,
"namespace": release.Namespace,
}).Error("provider.helm3: failed to process versioned release")
continue
}
if update {
helm3VersionedUpdatesCounter.With(prometheus.Labels{"chart": fmt.Sprintf("%s/%s", release.Namespace, release.Name)}).Inc()
plans = append(plans, plan)
}
}
return plans, nil
}
func (p *Provider) applyPlans(plans []*UpdatePlan) error {
for _, plan := range plans {
p.sender.Send(types.EventNotification{
ResourceKind: "chart",
Identifier: fmt.Sprintf("%s/%s/%s", "chart", plan.Namespace, plan.Name),
Name: "update release",
Message: fmt.Sprintf("Preparing to update release %s/%s %s->%s (%s)", plan.Namespace, plan.Name, plan.CurrentVersion, plan.NewVersion, strings.Join(mapToSlice(plan.Values), ", ")),
CreatedAt: time.Now(),
Type: types.NotificationPreReleaseUpdate,
Level: types.LevelDebug,
Channels: plan.Config.NotificationChannels,
Metadata: map[string]string{
"provider": p.GetName(),
"namespace": plan.Namespace,
"name": plan.Name,
},
})
// err := updateHelmRelease(p.implementer, plan.Name, plan.Chart, plan.Values)
err := updateHelmRelease(p.implementer, plan.Name, plan.Chart, plan.Values, plan.Namespace, plan.EmptyConfig)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"name": plan.Name,
"namespace": plan.Namespace,
}).Error("provider.helm3: failed to apply plan")
p.sender.Send(types.EventNotification{
ResourceKind: "chart",
Identifier: fmt.Sprintf("%s/%s/%s", "chart", plan.Namespace, plan.Name),
Name: "update release",
Message: fmt.Sprintf("Release update failed %s/%s %s->%s (%s), error: %s", plan.Namespace, plan.Name, plan.CurrentVersion, plan.NewVersion, strings.Join(mapToSlice(plan.Values), ", "), err),
CreatedAt: time.Now(),
Type: types.NotificationReleaseUpdate,
Level: types.LevelError,
Channels: plan.Config.NotificationChannels,
Metadata: map[string]string{
"provider": p.GetName(),
"namespace": plan.Namespace,
"name": plan.Name,
},
})
continue
}
err = p.updateComplete(plan)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"name": plan.Name,
"namespace": plan.Namespace,
}).Debug("provider.helm3: got error while resetting approvals counter after successful update")
}
var msg string
if len(plan.ReleaseNotes) == 0 {
msg = fmt.Sprintf("Successfully updated release %s/%s %s->%s (%s)", plan.Namespace, plan.Name, plan.CurrentVersion, plan.NewVersion, strings.Join(mapToSlice(plan.Values), ", "))
} else {
msg = fmt.Sprintf("Successfully updated release %s/%s %s->%s (%s). Release notes: %s", plan.Namespace, plan.Name, plan.CurrentVersion, plan.NewVersion, strings.Join(mapToSlice(plan.Values), ", "), strings.Join(plan.ReleaseNotes, ", "))
}
p.sender.Send(types.EventNotification{
ResourceKind: "chart",
Identifier: fmt.Sprintf("%s/%s/%s", "chart", plan.Namespace, plan.Name),
Name: "update release",
Message: msg,
CreatedAt: time.Now(),
Type: types.NotificationReleaseUpdate,
Level: types.LevelSuccess,
Channels: plan.Config.NotificationChannels,
Metadata: map[string]string{
"provider": p.GetName(),
"namespace": plan.Namespace,
"name": plan.Name,
},
})
}
return nil
}
func updateHelmRelease(implementer Implementer, releaseName string, chart *hapi_chart.Chart, overrideValues map[string]string, namespace string, opts ...bool) error {
// set reuse values to false if currentRelease.config is nil
emptyConfig := false
if len(opts) == 1 && opts[0] {
emptyConfig = opts[0]
}
resp, err := implementer.UpdateReleaseFromChart(releaseName, chart, overrideValues, namespace, emptyConfig)
if err != nil {
return err
}
log.WithFields(log.Fields{
"version": resp.Version,
"release": releaseName,
"overrideValues": overrideValues,
}).Info("provider.helm3: release updated")
return nil
}
func mapToSlice(values map[string]string) []string {
converted := []string{}
for k, v := range values {
concat := k + "=" + v
converted = append(converted, concat)
}
return converted
}
// parse
func convertToYaml(values []string) ([]byte, error) {
base := map[string]interface{}{}
for _, value := range values {
if err := strvals.ParseInto(value, base); err != nil {
return []byte{}, fmt.Errorf("failed parsing --set data: %s", err)
}
}
return yaml.Marshal(base)
}
func getValueAsString(vals chartutil.Values, path string) (string, error) {
valinterface, err := vals.PathValue(path)
if err != nil {
return "", err
}
valString := fmt.Sprintf("%v", valinterface)
return valString, nil
}
// func values(chart *hapi_chart.Chart, config *hapi_chart.Config) (chartutil.Values, error) {
func values(chart *hapi_chart.Chart, config map[string]interface{}) (chartutil.Values, error) {
return chartutil.CoalesceValues(chart, config)
}
func getKeelConfig(vals chartutil.Values) (*KeelChartConfig, error) {
yamlFull, err := vals.YAML()
if err != nil {
return nil, fmt.Errorf("failed to get vals config, error: %s", err)
}
var r Root
// Default MatchPreRelease to true if not present (backward compatibility)
r.Keel.MatchPreRelease = true
err = yaml.Unmarshal([]byte(yamlFull), &r)
if err != nil {
return nil, fmt.Errorf("failed to parse keel config: %s", err)
}
if r.Keel.Policy == "" {
return nil, ErrPolicyNotSpecified
}
cfg := r.Keel
cfg.Plc = policy.GetPolicy(cfg.Policy, &policy.Options{MatchTag: cfg.MatchTag, MatchPreRelease: cfg.MatchPreRelease})
return &cfg, nil
}

View File

@ -0,0 +1,742 @@
package helm3
import (
"io/ioutil"
"log"
"os"
"path/filepath"
"reflect"
"testing"
"github.com/keel-hq/keel/approvals"
"github.com/keel-hq/keel/extension/notification"
"github.com/keel-hq/keel/internal/policy"
"github.com/keel-hq/keel/pkg/store/sql"
"github.com/keel-hq/keel/types"
"sigs.k8s.io/yaml"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/release"
)
func newTestingUtils() (*sql.SQLStore, func()) {
dir, err := ioutil.TempDir("", "whstoretest")
if err != nil {
log.Fatal(err)
}
tmpfn := filepath.Join(dir, "gorm.db")
// defer
store, err := sql.New(sql.Opts{DatabaseType: "sqlite3", URI: tmpfn})
if err != nil {
log.Fatal(err)
}
teardown := func() {
os.RemoveAll(dir) // clean up
}
return store, teardown
}
func approver() (*approvals.DefaultManager, func()) {
store, teardown := newTestingUtils()
return approvals.New(&approvals.Opts{
Store: store,
}), teardown
}
type fakeSender struct {
sentEvent types.EventNotification
}
func (s *fakeSender) Configure(cfg *notification.Config) (bool, error) {
return true, nil
}
func (s *fakeSender) Send(event types.EventNotification) error {
s.sentEvent = event
return nil
}
type fakeImplementer struct {
listReleasesResponse []*release.Release
// updated info
updatedRlsName string
updatedChart *chart.Chart
// updatedOptions []helm.UpdateOption
}
func (i *fakeImplementer) ListReleases() ([]*release.Release, error) {
return i.listReleasesResponse, nil
}
func (i *fakeImplementer) UpdateReleaseFromChart(rlsName string, chart *chart.Chart, vals map[string]string, namespace string, opts ...bool) (*release.Release, error) {
// func (i *fakeImplementer) UpdateReleaseFromChart(rlsName string, chart *chart.Chart, opts ...helm.UpdateOption) (*rls.UpdateReleaseResponse, error) {
i.updatedRlsName = rlsName
i.updatedChart = chart
// i.updatedOptions = opts
return &release.Release{
Name: rlsName,
Chart: chart,
Version: 2,
}, nil
}
// helper function to generate keel configuration
func testingConfigYaml(cfg *KeelChartConfig) (vals chartutil.Values, err error) {
root := &Root{Keel: *cfg}
bts, err := yaml.Marshal(root)
if err != nil {
return nil, err
}
return chartutil.ReadValues(bts)
}
// helper function to generate chart.Chart object from string
func testingStringToChart(raw string) (myChart *chart.Chart, err error) {
chartVals, err := chartutil.ReadValues([]byte(raw))
if err != nil {
return nil, err
}
myChart = &chart.Chart{
Values: chartVals,
Metadata: &chart.Metadata{Name: "app-x"},
}
return myChart, nil
}
func TestGetChartPolicy(t *testing.T) {
chartVals := `
name: al Rashid
where:
city: Basrah
title: caliph
image:
repository: gcr.io/v2-namespace/hello-world
tag: 1.1.0
keel:
policy: all
trigger: poll
images:
- repository: image.repository
tag: image.tag
`
myChart, err := testingStringToChart(chartVals)
if err != nil {
t.Errorf("chartutil.ReadValues error = %v", err)
}
fakeImpl := &fakeImplementer{
listReleasesResponse: []*release.Release{
{
Name: "release-1",
Chart: myChart,
Config: make(map[string]interface{}),
},
},
}
releases, err := fakeImpl.ListReleases()
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if len(releases) == 0 {
t.Fatalf("unexpexted error: releases should not be empty")
}
policyFound := false
for _, release := range releases {
vals, err := values(release.Chart, release.Config)
if err != nil {
t.Fatalf("failed to get values: %s", err)
}
cfg, err := getKeelConfig(vals)
if err != nil {
t.Errorf("failed to get image paths: %s", err)
}
if cfg.Plc.Name() == policy.SemverPolicyTypeAll.String() {
policyFound = true
}
}
if !policyFound {
t.Errorf("policy not found")
}
}
func TestGetChartPolicyFromProm(t *testing.T) {
myChart, err := testingStringToChart(promChartValues)
if err != nil {
t.Errorf("chartutil.ReadValues error = %v", err)
}
fakeImpl := &fakeImplementer{
listReleasesResponse: []*release.Release{
{
Name: "release-1",
Chart: myChart,
Config: make(map[string]interface{}),
},
},
}
releases, err := fakeImpl.ListReleases()
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
policyFound := false
for _, release := range releases {
vals, err := values(release.Chart, release.Config)
if err != nil {
t.Fatalf("failed to get values: %s", err)
}
cfg, err := getKeelConfig(vals)
if err != nil {
t.Errorf("failed to get image paths: %s", err)
}
if cfg.Plc.Name() == policy.SemverPolicyTypeAll.String() {
policyFound = true
}
}
if !policyFound {
t.Errorf("policy not found")
}
}
func TestGetTrackedReleases(t *testing.T) {
chartVals := `
name: chart-x
where:
city: kaunas
title: hmm
image:
repository: gcr.io/v2-namespace/bye-world
tag: 1.1.0
image2:
repository: gcr.io/v2-namespace/hello-world
tag: 1.2.0
keel:
policy: all
trigger: poll
images:
- repository: image.repository
tag: image.tag
`
myChart, err := testingStringToChart(chartVals)
if err != nil {
t.Errorf("chartutil.ReadValues error = %v", err)
}
fakeImpl := &fakeImplementer{
listReleasesResponse: []*release.Release{
{
Name: "release-1",
Chart: myChart,
Config: make(map[string]interface{}),
},
},
}
approver, teardown := approver()
defer teardown()
prov := NewProvider(fakeImpl, &fakeSender{}, approver)
tracked, _ := prov.TrackedImages()
if tracked[0].Image.Remote() != "gcr.io/v2-namespace/bye-world:1.1.0" {
t.Errorf("unexpected image: %s", tracked[0].Image.Remote())
}
}
func TestGetTrackedReleasesWithoutKeelConfig(t *testing.T) {
chartVals := `
name: chart-x
where:
city: kaunas
title: hmm
image:
repository: gcr.io/v2-namespace/bye-world
tag: 1.1.0
image2:
repository: gcr.io/v2-namespace/hello-world
tag: 1.2.0
`
myChart, err := testingStringToChart(chartVals)
if err != nil {
t.Errorf("chartutil.ReadValues error = %v", err)
}
fakeImpl := &fakeImplementer{
listReleasesResponse: []*release.Release{
{
Name: "release-1",
Chart: myChart,
Config: make(map[string]interface{}),
},
},
}
approver, teardown := approver()
defer teardown()
prov := NewProvider(fakeImpl, &fakeSender{}, approver)
tracked, _ := prov.TrackedImages()
if len(tracked) != 0 {
t.Errorf("didn't expect to find any tracked releases, found: %d", len(tracked))
}
}
func TestGetTrackedReleasesTotallyNonStandard(t *testing.T) {
chartVals := `
name: chart-x
where:
city: kaunas
title: hmm
ihavemyownstandard:
repo: gcr.io/v2-namespace/bye-world
version: 1.1.0
image2:
repository: gcr.io/v2-namespace/hello-world
tag: 1.2.0
keel:
policy: all
trigger: poll
images:
- repository: ihavemyownstandard.repo
tag: ihavemyownstandard.version
`
myChart, err := testingStringToChart(chartVals)
if err != nil {
t.Errorf("chartutil.ReadValues error = %v", err)
}
fakeImpl := &fakeImplementer{
listReleasesResponse: []*release.Release{
{
Name: "release-1",
Chart: myChart,
Config: make(map[string]interface{}),
},
},
}
approver, teardown := approver()
defer teardown()
prov := NewProvider(fakeImpl, &fakeSender{}, approver)
tracked, _ := prov.TrackedImages()
if tracked[0].Image.Remote() != "gcr.io/v2-namespace/bye-world:1.1.0" {
t.Errorf("unexpected image: %s", tracked[0].Image.Remote())
}
}
func TestGetTriggerFromConfig(t *testing.T) {
vals, err := testingConfigYaml(&KeelChartConfig{Trigger: types.TriggerTypePoll, Policy: "all"})
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
cfg, err := getKeelConfig(vals)
if err != nil {
t.Fatalf("failed to get image paths: %s", err)
}
if cfg.Trigger != types.TriggerTypePoll {
t.Errorf("invalid trigger: %s", cfg.Trigger)
}
}
func TestGetPolicyFromConfig(t *testing.T) {
vals, err := testingConfigYaml(&KeelChartConfig{Policy: "all"})
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
cfg, err := getKeelConfig(vals)
if err != nil {
t.Errorf("failed to get image paths: %s", err)
}
// if cfg.Policy != types.PolicyTypeAll {
if cfg.Plc.Name() != policy.SemverPolicyTypeAll.String() {
t.Errorf("invalid policy: %s", cfg.Policy)
}
}
func TestGetImagesFromConfig(t *testing.T) {
vals, err := testingConfigYaml(&KeelChartConfig{Policy: "all", Images: []ImageDetails{
{
RepositoryPath: "repopath",
TagPath: "tagpath",
},
}})
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
cfg, err := getKeelConfig(vals)
if err != nil {
t.Errorf("failed to get image paths: %s", err)
}
if cfg.Images[0].RepositoryPath != "repopath" {
t.Errorf("invalid repo path: %s", cfg.Images[0].RepositoryPath)
}
if cfg.Images[0].TagPath != "tagpath" {
t.Errorf("invalid tag path: %s", cfg.Images[0].TagPath)
}
}
func TestUpdateRelease(t *testing.T) {
chartVals := `
name: al Rashid
where:
city: Basrah
title: caliph
image:
repository: karolisr/webhook-demo
tag: 0.0.10
keel:
policy: all
trigger: poll
images:
- repository: image.repository
tag: image.tag
`
myChart, err := testingStringToChart(chartVals)
if err != nil {
t.Errorf("chartutil.ReadValues error = %v", err)
}
fakeImpl := &fakeImplementer{
listReleasesResponse: []*release.Release{
{
Name: "release-1",
Chart: myChart,
Config: make(map[string]interface{}),
},
},
}
approver, teardown := approver()
defer teardown()
provider := NewProvider(fakeImpl, &fakeSender{}, approver)
err = provider.processEvent(&types.Event{
Repository: types.Repository{
Name: "karolisr/webhook-demo",
Tag: "0.0.11",
},
})
if err != nil {
t.Errorf("failed to process event, error: %s", err)
}
// checking updated release
if fakeImpl.updatedChart != myChart {
t.Errorf("wrong chart updated")
}
if fakeImpl.updatedRlsName != "release-1" {
t.Errorf("unexpected release updated: %s", fakeImpl.updatedRlsName)
}
}
var pollingValues = `
name: al Rashid
where:
city: Basrah
title: caliph
image:
repository: gcr.io/v2-namespace/hello-world
tag: 1.1.0
keel:
policy: all
trigger: poll
pollSchedule: "@every 12m"
images:
- repository: image.repository
tag: image.tag
`
func TestGetPollingSchedule(t *testing.T) {
vals, _ := chartutil.ReadValues([]byte(pollingValues))
cfg, err := getKeelConfig(vals)
if err != nil {
t.Errorf("failed to get config: %s", err)
}
if cfg.PollSchedule != "@every 12m" {
t.Errorf("unexpected polling schedule: %s", cfg.PollSchedule)
}
}
func Test_getKeelConfig(t *testing.T) {
var valuesBasicStr = `
name: al Rashid
where:
city: Basrah
title: caliph
image:
repository: gcr.io/v2-namespace/hello-world
tag: 1.1.0
keel:
policy: all
images:
- repository: image.repository
tag: image.tag
`
valuesBasic, _ := chartutil.ReadValues([]byte(valuesBasicStr))
var valuesChannelsStr = `
name: al Rashid
where:
city: Basrah
title: caliph
image:
repository: gcr.io/v2-namespace/hello-world
tag: 1.1.0
keel:
policy: all
notificationChannels:
- chan1
- chan2
images:
- repository: image.repository
tag: image.tag
`
valuesChannels, _ := chartutil.ReadValues([]byte(valuesChannelsStr))
var valuesPollStr = `
name: al Rashid
where:
city: Basrah
title: caliph
image:
repository: gcr.io/v2-namespace/hello-world
tag: 1.1.0
keel:
policy: major
trigger: poll
pollSchedule: "@every 30m"
images:
- repository: image.repository
tag: image.tag
imagePullSecret: such-secret
`
valuesPoll, _ := chartutil.ReadValues([]byte(valuesPollStr))
var valuesNoMatchPreReleaseStr = `
name: al Rashid
where:
city: Basrah
title: caliph
image:
repository: gcr.io/v2-namespace/hello-world
tag: 1.1.0
keel:
policy: all
matchPreRelease: false
images:
- repository: image.repository
tag: image.tag
`
valuesNoMatchPreRelease, _ := chartutil.ReadValues([]byte(valuesNoMatchPreReleaseStr))
type args struct {
vals chartutil.Values
}
tests := []struct {
name string
args args
want *KeelChartConfig
wantErr bool
}{
{
name: "correct config",
args: args{vals: valuesBasic},
want: &KeelChartConfig{
Policy: "all",
MatchPreRelease: true,
Trigger: types.TriggerTypeDefault,
Images: []ImageDetails{
{RepositoryPath: "image.repository", TagPath: "image.tag"},
},
Plc: policy.NewSemverPolicy(policy.SemverPolicyTypeAll, true),
},
},
{
name: "custom notification channels",
args: args{vals: valuesChannels},
want: &KeelChartConfig{
Policy: "all",
MatchPreRelease: true,
Trigger: types.TriggerTypeDefault,
NotificationChannels: []string{"chan1", "chan2"},
Images: []ImageDetails{
{RepositoryPath: "image.repository", TagPath: "image.tag"},
},
Plc: policy.NewSemverPolicy(policy.SemverPolicyTypeAll, true),
},
},
{
name: "correct polling config",
args: args{vals: valuesPoll},
want: &KeelChartConfig{
Policy: "major",
MatchPreRelease: true,
Trigger: types.TriggerTypePoll,
PollSchedule: "@every 30m",
Images: []ImageDetails{
{RepositoryPath: "image.repository", TagPath: "image.tag", ImagePullSecret: "such-secret"},
},
Plc: policy.NewSemverPolicy(policy.SemverPolicyTypeMajor, true),
},
},
{
name: "disable matchPreRelease",
args: args{vals: valuesNoMatchPreRelease},
want: &KeelChartConfig{
Policy: "all",
MatchPreRelease: false,
Trigger: types.TriggerTypeDefault,
Images: []ImageDetails{
{RepositoryPath: "image.repository", TagPath: "image.tag"},
},
Plc: policy.NewSemverPolicy(policy.SemverPolicyTypeAll, false),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := getKeelConfig(tt.args.vals)
if (err != nil) != tt.wantErr {
t.Errorf("getKeelConfig() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("getKeelConfig() = %v, want %v", got, tt.want)
}
})
}
}
func TestGetChartMatchTag(t *testing.T) {
chartVals := `
name: al Rashid
where:
city: Basrah
title: caliph
image:
repository: gcr.io/v2-namespace/hello-world
tag: 1.1.0
keel:
policy: all
trigger: poll
matchTag: true
images:
- repository: image.repository
tag: image.tag
`
myChart, err := testingStringToChart(chartVals)
if err != nil {
t.Errorf("chartutil.ReadValues error = %v", err)
}
fakeImpl := &fakeImplementer{
listReleasesResponse: []*release.Release{
{
Name: "release-1",
Chart: myChart,
Config: make(map[string]interface{}),
},
},
}
releases, err := fakeImpl.ListReleases()
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
policyFound := false
for _, release := range releases {
vals, err := values(release.Chart, release.Config)
if err != nil {
t.Fatalf("failed to get values: %s", err)
}
cfg, err := getKeelConfig(vals)
if err != nil {
t.Errorf("failed to get image paths: %s", err)
}
if cfg.Plc.Name() == policy.SemverPolicyTypeAll.String() {
policyFound = true
}
if !cfg.MatchTag {
t.Errorf("expected to find 'matchTag' == true ")
}
}
if !policyFound {
t.Errorf("policy not found")
}
}

View File

@ -0,0 +1,128 @@
package helm3
import (
"os"
"strings"
"helm.sh/helm/v3/pkg/chart"
log "github.com/sirupsen/logrus"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/release"
// "helm.sh/helm/v3/pkg/cli"
"k8s.io/cli-runtime/pkg/genericclioptions"
)
// to do:
// * update to latest chart package
// * udpate the paramateres for the function
const DefaultUpdateTimeout = 300
// Implementer - generic helm implementer used to abstract actual implementation
type Implementer interface {
// ListReleases(opts ...helm.ReleaseListOption) ([]*release.Release, error)
ListReleases() ([]*release.Release, error)
UpdateReleaseFromChart(rlsName string, chart *chart.Chart, vals map[string]string, namespace string, opts ...bool) (*release.Release, error)
}
// Helm3Implementer - actual helm3 implementer
type Helm3Implementer struct {
// actionConfig *action.Configuration
HelmDriver string
KubeContext string
KubeToken string
KubeAPIServer string
}
// NewHelm3Implementer - get new helm implementer
func NewHelm3Implementer() *Helm3Implementer {
return &Helm3Implementer{
HelmDriver: os.Getenv("HELM_DRIVER"),
KubeContext: os.Getenv("HELM_KUBECONTEXT"),
KubeToken: os.Getenv("HELM_KUBETOKEN"),
KubeAPIServer: os.Getenv("HELM_KUBEAPISERVER"),
}
}
// ListReleases - list available releases
func (i *Helm3Implementer) ListReleases() ([]*release.Release, error) {
actionConfig := i.generateConfig("")
client := action.NewList(actionConfig)
results, err := client.Run()
if err != nil {
log.WithFields(log.Fields{
"error": err,
}).Fatal("helm3: failed to list release")
return []*release.Release{}, err
}
return results, nil
}
// UpdateReleaseFromChart - update release from chart
func (i *Helm3Implementer) UpdateReleaseFromChart(rlsName string, chart *chart.Chart, vals map[string]string, namespace string, opts ...bool) (*release.Release, error) {
actionConfig := i.generateConfig(namespace)
client := action.NewUpgrade(actionConfig)
client.Namespace = namespace
client.Force = true
client.Timeout = DefaultUpdateTimeout
client.ReuseValues = true
// set reuse values to false if currentRelease.config is nil (temp fix for bug in chartutil.coalesce v3.1.2)
if len(opts) == 1 && opts[0] {
client.ReuseValues = false
}
convertedVals := convertToInterface(vals)
// returns the new release
results, err := client.Run(rlsName, chart, convertedVals)
if err != nil {
log.WithFields(log.Fields{
"error": err,
}).Fatal("helm3: failed to update release from chart")
return nil, err
}
return results, err
}
func (i *Helm3Implementer) generateConfig(namespace string) *action.Configuration {
// settings := cli.New()
config := &genericclioptions.ConfigFlags{
Namespace: &namespace,
Context: &i.KubeContext,
BearerToken: &i.KubeToken,
APIServer: &i.KubeAPIServer,
}
actionConfig := &action.Configuration{}
if err := actionConfig.Init(config, namespace, i.HelmDriver, log.Printf); err != nil {
log.Printf("%+v", err)
os.Exit(1)
}
return actionConfig
}
// convert map[string]string to map[string]interface
// converts:
// map[string]string{"image.tag": "0.1.0"}
// to:
// map[string]interface{"image": map[string]interface{"tag": "0.1.0"}}
func convertToInterface(values map[string]string) map[string]interface{} {
converted := make(map[string]interface{})
for key, value := range values {
keys := strings.SplitN(key, ".", 2)
if len(keys) == 1 {
converted[key] = value
} else if len(keys) == 2 {
converted[keys[0]] = convertToInterface(map[string]string{
keys[1]: value,
})
}
}
return converted
}

View File

@ -0,0 +1,20 @@
package helm3
import (
"testing"
)
func TestImplementerList(t *testing.T) {
t.Skip()
imp := NewHelm3Implementer()
releases, err := imp.ListReleases()
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if len(releases) == 0 {
t.Errorf("why no releases? ")
}
}

120
provider/helm3/updates.go Normal file
View File

@ -0,0 +1,120 @@
package helm3
import (
"github.com/keel-hq/keel/internal/policy"
"github.com/keel-hq/keel/types"
"github.com/keel-hq/keel/util/image"
hapi_chart "helm.sh/helm/v3/pkg/chart"
log "github.com/sirupsen/logrus"
)
func checkRelease(repo *types.Repository, namespace, name string, chart *hapi_chart.Chart, config map[string]interface{}) (plan *UpdatePlan, shouldUpdateRelease bool, err error) {
plan = &UpdatePlan{
Chart: chart,
Namespace: namespace,
Name: name,
Values: make(map[string]string),
EmptyConfig: config == nil,
}
eventRepoRef, err := image.Parse(repo.String())
if err != nil {
log.WithFields(log.Fields{
"error": err,
"repository_name": repo.Name,
}).Error("provider.helm3: failed to parse event repository name")
return
}
// getting configuration
vals, err := values(chart, config)
if err != nil {
log.WithFields(log.Fields{
"error": err,
}).Error("provider.helm3: failed to get values.yaml for release")
return
}
keelCfg, err := getKeelConfig(vals)
if err != nil {
if err == ErrPolicyNotSpecified {
// nothing to do
return plan, false, nil
}
log.WithFields(log.Fields{
"error": err,
}).Error("provider.helm3: failed to get keel configuration for release")
// ignoring this release, no keel config found
return plan, false, nil
}
log.Infof("policy for release %s/%s parsed: %s", namespace, name, keelCfg.Plc.Name())
if keelCfg.Plc.Type() == policy.PolicyTypeNone {
// policy is not set, ignoring release
return plan, false, nil
}
// checking for impacted images
for _, imageDetails := range keelCfg.Images {
imageRef, err := parseImage(vals, &imageDetails)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"repository_name": imageDetails.RepositoryPath,
"repository_tag": imageDetails.TagPath,
}).Error("provider.helm3: failed to parse image")
continue
}
if imageRef.Repository() != eventRepoRef.Repository() {
log.WithFields(log.Fields{
"parsed_image_name": imageRef.Remote(),
"target_image_name": repo.Name,
}).Debug("provider.helm3: images do not match, ignoring")
continue
}
shouldUpdate, err := keelCfg.Plc.ShouldUpdate(imageRef.Tag(), eventRepoRef.Tag())
if err != nil {
log.WithFields(log.Fields{
"error": err,
"repository_name": imageDetails.RepositoryPath,
"repository_tag": imageDetails.TagPath,
}).Error("provider.helm3: got error while checking whether update the chart")
continue
}
if !shouldUpdate {
log.WithFields(log.Fields{
"parsed_image_name": imageRef.Remote(),
"target_image_name": repo.Name,
"policy": keelCfg.Plc.Name(),
}).Info("provider.helm3: ignoring")
continue
}
if imageDetails.DigestPath != "" {
plan.Values[imageDetails.DigestPath] = repo.Digest
log.WithFields(log.Fields{
"image_details_digestPath": imageDetails.DigestPath,
"target_image_digest": repo.Digest,
}).Debug("provider.helm3: setting image Digest")
}
path, value := getUnversionedPlanValues(repo.Tag, imageRef, &imageDetails)
plan.Values[path] = value
plan.NewVersion = repo.Tag
plan.CurrentVersion = imageRef.Tag()
plan.Config = keelCfg
shouldUpdateRelease = true
if imageDetails.ReleaseNotes != "" {
plan.ReleaseNotes = append(plan.ReleaseNotes, imageDetails.ReleaseNotes)
}
}
return plan, shouldUpdateRelease, nil
}

View File

@ -0,0 +1,513 @@
package helm3
import (
"reflect"
"testing"
"github.com/keel-hq/keel/internal/policy"
"github.com/keel-hq/keel/types"
hapi_chart "helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chartutil"
)
func Test_checkUnversionedRelease(t *testing.T) {
chartValuesPolicyForce := `
name: al Rashid
where:
city: Basrah
title: caliph
image:
repository: gcr.io/v2-namespace/hello-world
tag: 1.1.0
keel:
policy: force
trigger: poll
images:
- repository: image.repository
tag: image.tag
`
chartValuesPolicyForceReleaseNotes := `
name: al Rashid
where:
city: Basrah
title: caliph
image:
repository: gcr.io/v2-namespace/hello-world
tag: 1.1.0
keel:
policy: force
trigger: poll
images:
- repository: image.repository
tag: image.tag
releaseNotes: https://github.com/keel-hq/keel/releases
`
chartValuesPolicyMajor := `
name: al Rashid
where:
city: Basrah
title: caliph
image:
repository: gcr.io/v2-namespace/hello-world
tag: 1.1.0
keel:
policy: major
trigger: poll
images:
- repository: image.repository
tag: image.tag
`
chartValuesPolicyForceVal, err := chartutil.ReadValues([]byte(chartValuesPolicyForce))
if err != nil {
t.Errorf("chartutil.ReadValues error = %v", err)
}
chartValuesPolicyMajorVal, err := chartutil.ReadValues([]byte(chartValuesPolicyMajor))
if err != nil {
t.Errorf("chartutil.ReadValues error = %v", err)
}
chartValuesPolicyForceReleaseNotesVal, err := chartutil.ReadValues([]byte(chartValuesPolicyForceReleaseNotes))
if err != nil {
t.Errorf("chartutil.ReadValues error = %v", err)
}
helloWorldChart := &hapi_chart.Chart{
// Values: &hapi_chart.Config{Raw: chartValuesPolicyForce},
Values: chartValuesPolicyForceVal,
Metadata: &hapi_chart.Metadata{Name: "app-x"},
}
helloWorldChartPolicyMajor := &hapi_chart.Chart{
Values: chartValuesPolicyMajorVal,
Metadata: &hapi_chart.Metadata{Name: "app-x"},
}
helloWorldChartPolicyMajorReleaseNotes := &hapi_chart.Chart{
Values: chartValuesPolicyForceReleaseNotesVal,
Metadata: &hapi_chart.Metadata{Name: "app-x"},
}
type args struct {
repo *types.Repository
namespace string
name string
chart *hapi_chart.Chart
config map[string]interface{}
}
tests := []struct {
name string
args args
wantPlan *UpdatePlan
wantShouldUpdateRelease bool
wantErr bool
}{
{
name: "correct force update",
args: args{
repo: &types.Repository{Name: "gcr.io/v2-namespace/hello-world", Tag: "latest"},
namespace: "default",
name: "release-1",
chart: helloWorldChart,
config: make(map[string]interface{}),
},
wantPlan: &UpdatePlan{
Namespace: "default",
Name: "release-1",
Chart: helloWorldChart,
Values: map[string]string{"image.tag": "latest"},
CurrentVersion: "1.1.0",
NewVersion: "latest",
Config: &KeelChartConfig{
Policy: "force",
MatchPreRelease: true,
Trigger: types.TriggerTypePoll,
Images: []ImageDetails{
{
RepositoryPath: "image.repository",
TagPath: "image.tag",
},
},
Plc: policy.NewForcePolicy(false),
},
},
wantShouldUpdateRelease: true,
wantErr: false,
},
{
name: "correct force update, with release notes",
args: args{
repo: &types.Repository{Name: "gcr.io/v2-namespace/hello-world", Tag: "1.2.0"},
namespace: "default",
name: "release-1",
chart: helloWorldChartPolicyMajorReleaseNotes,
config: make(map[string]interface{}),
},
wantPlan: &UpdatePlan{
Namespace: "default",
Name: "release-1",
Chart: helloWorldChartPolicyMajorReleaseNotes,
Values: map[string]string{"image.tag": "1.2.0"},
CurrentVersion: "1.1.0",
NewVersion: "1.2.0",
ReleaseNotes: []string{"https://github.com/keel-hq/keel/releases"},
Config: &KeelChartConfig{
Policy: "force",
MatchPreRelease: true,
Trigger: types.TriggerTypePoll,
Images: []ImageDetails{
{
RepositoryPath: "image.repository",
TagPath: "image.tag",
ReleaseNotes: "https://github.com/keel-hq/keel/releases",
},
},
Plc: policy.NewForcePolicy(false),
},
},
wantShouldUpdateRelease: true,
wantErr: false,
},
{
name: "update without force",
args: args{
repo: &types.Repository{Name: "gcr.io/v2-namespace/hello-world", Tag: "latest"},
namespace: "default",
name: "release-1",
chart: helloWorldChartPolicyMajor,
config: make(map[string]interface{}),
},
wantPlan: &UpdatePlan{
Namespace: "default",
Name: "release-1",
Chart: helloWorldChartPolicyMajor,
Values: map[string]string{}},
wantShouldUpdateRelease: false,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotPlan, gotShouldUpdateRelease, err := checkRelease(tt.args.repo, tt.args.namespace, tt.args.name, tt.args.chart, tt.args.config)
if (err != nil) != tt.wantErr {
t.Errorf("checkRelease() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(gotPlan, tt.wantPlan) {
t.Errorf("checkRelease() gotPlan = %v, want %v", gotPlan, tt.wantPlan)
}
if gotShouldUpdateRelease != tt.wantShouldUpdateRelease {
t.Errorf("checkRelease() gotShouldUpdateRelease = %v, want %v", gotShouldUpdateRelease, tt.wantShouldUpdateRelease)
}
})
}
}
func Test_checkRelease(t *testing.T) {
chartValuesA := `
name: al Rashid
where:
city: Basrah
title: caliph
image:
repository: gcr.io/v2-namespace/hello-world
tag: 1.1.0
keel:
policy: all
trigger: poll
images:
- repository: image.repository
tag: image.tag
`
// non semver existing
chartValuesB := `
name: al Rashid
where:
city: Basrah
title: caliph
image:
repository: gcr.io/v2-namespace/hello-world
tag: alpha
keel:
policy: force
trigger: poll
images:
- repository: image.repository
tag: image.tag
`
chartValuesNonSemverNoForce := `
name: al Rashid
where:
city: Basrah
title: caliph
image:
repository: gcr.io/v2-namespace/hello-world
tag: alpha
keel:
policy: major
trigger: poll
images:
- repository: image.repository
tag: image.tag
`
chartValuesNoTag := `
name: al Rashid
where:
city: Basrah
title: caliph
image:
repository: gcr.io/v2-namespace/hello-world:1.0.0
keel:
policy: major
trigger: poll
images:
- repository: image.repository
`
chartValuesNoKeelCfg := `
name: al Rashid
where:
city: Basrah
title: caliph
image:
repository: gcr.io/v2-namespace/hello-world
tag: 1.0.0
`
chartValuesAVal, err := chartutil.ReadValues([]byte(chartValuesA))
if err != nil {
t.Errorf("chartutil.ReadValues error = %v", err)
}
chartValuesBVal, err := chartutil.ReadValues([]byte(chartValuesB))
if err != nil {
t.Errorf("chartutil.ReadValues error = %v", err)
}
chartValuesNonSemverNoForceVal, err := chartutil.ReadValues([]byte(chartValuesNonSemverNoForce))
if err != nil {
t.Errorf("chartutil.ReadValues error = %v", err)
}
chartValuesNoTagVal, err := chartutil.ReadValues([]byte(chartValuesNoTag))
if err != nil {
t.Errorf("chartutil.ReadValues error = %v", err)
}
chartValuesNoKeelCfgVal, err := chartutil.ReadValues([]byte(chartValuesNoKeelCfg))
if err != nil {
t.Errorf("chartutil.ReadValues error = %v", err)
}
helloWorldChart := &hapi_chart.Chart{
Values: chartValuesAVal,
Metadata: &hapi_chart.Metadata{Name: "app-x"},
}
helloWorldNonSemverChart := &hapi_chart.Chart{
Values: chartValuesBVal,
Metadata: &hapi_chart.Metadata{Name: "app-x"},
}
helloWorldNonSemverNoForceChart := &hapi_chart.Chart{
Values: chartValuesNonSemverNoForceVal,
Metadata: &hapi_chart.Metadata{Name: "app-x"},
}
helloWorldNoTagChart := &hapi_chart.Chart{
Values: chartValuesNoTagVal,
Metadata: &hapi_chart.Metadata{Name: "app-x"},
}
helloWorldNoKeelCfg := &hapi_chart.Chart{
Values: chartValuesNoKeelCfgVal,
Metadata: &hapi_chart.Metadata{Name: "app-x"},
}
type args struct {
repo *types.Repository
namespace string
name string
chart *hapi_chart.Chart
config map[string]interface{}
}
tests := []struct {
name string
args args
wantPlan *UpdatePlan
wantShouldUpdateRelease bool
wantErr bool
}{
{
name: "correct version bump",
args: args{
// newVersion: unsafeGetVersion("1.1.2"),
repo: &types.Repository{Name: "gcr.io/v2-namespace/hello-world", Tag: "1.1.2"},
namespace: "default",
name: "release-1",
chart: helloWorldChart,
config: make(map[string]interface{}),
},
wantPlan: &UpdatePlan{
Namespace: "default",
Name: "release-1",
Chart: helloWorldChart,
Values: map[string]string{"image.tag": "1.1.2"},
NewVersion: "1.1.2",
CurrentVersion: "1.1.0",
Config: &KeelChartConfig{
Policy: "all",
MatchPreRelease: true,
Trigger: types.TriggerTypePoll,
Images: []ImageDetails{
{RepositoryPath: "image.repository", TagPath: "image.tag"},
},
Plc: policy.NewSemverPolicy(policy.SemverPolicyTypeAll, true),
},
},
wantShouldUpdateRelease: true,
wantErr: false,
},
{
name: "correct but same version",
args: args{
repo: &types.Repository{Name: "gcr.io/v2-namespace/hello-world", Tag: "1.1.0"},
namespace: "default",
name: "release-1",
chart: helloWorldChart,
config: make(map[string]interface{}),
},
wantPlan: &UpdatePlan{Namespace: "default", Name: "release-1", Chart: helloWorldChart, Values: map[string]string{}},
wantShouldUpdateRelease: false,
wantErr: false,
},
{
name: "different image",
args: args{
repo: &types.Repository{Name: "gcr.io/v2-namespace/bye-world", Tag: "1.1.5"},
namespace: "default",
name: "release-1",
chart: helloWorldChart,
config: make(map[string]interface{}),
},
wantPlan: &UpdatePlan{Namespace: "default", Name: "release-1", Chart: helloWorldChart, Values: map[string]string{}},
wantShouldUpdateRelease: false,
wantErr: false,
},
{
name: "non semver existing version",
args: args{
repo: &types.Repository{Name: "gcr.io/v2-namespace/hello-world", Tag: "1.1.0"},
namespace: "default",
name: "release-1",
chart: helloWorldNonSemverChart,
config: make(map[string]interface{}),
},
wantPlan: &UpdatePlan{
Namespace: "default",
Name: "release-1",
Chart: helloWorldNonSemverChart,
Values: map[string]string{"image.tag": "1.1.0"},
NewVersion: "1.1.0",
CurrentVersion: "alpha",
Config: &KeelChartConfig{
Policy: "force",
MatchPreRelease: true,
Trigger: types.TriggerTypePoll,
Images: []ImageDetails{
{RepositoryPath: "image.repository", TagPath: "image.tag"},
},
Plc: policy.NewForcePolicy(false),
},
},
wantShouldUpdateRelease: true,
wantErr: false,
},
{
name: "non semver no force, should not add to plan",
args: args{
repo: &types.Repository{Name: "gcr.io/v2-namespace/hello-world", Tag: "1.1.0"},
namespace: "default",
name: "release-1",
chart: helloWorldNonSemverNoForceChart,
config: make(map[string]interface{}),
},
wantPlan: &UpdatePlan{Namespace: "default", Name: "release-1", Chart: helloWorldNonSemverNoForceChart, Values: map[string]string{}},
wantShouldUpdateRelease: false,
wantErr: false,
},
{
name: "semver no tag",
args: args{
repo: &types.Repository{Name: "gcr.io/v2-namespace/hello-world", Tag: "1.1.0"},
namespace: "default",
name: "release-1-no-tag",
chart: helloWorldNoTagChart,
config: make(map[string]interface{}),
},
wantPlan: &UpdatePlan{
Namespace: "default",
Name: "release-1-no-tag",
Chart: helloWorldNoTagChart,
Values: map[string]string{"image.repository": "gcr.io/v2-namespace/hello-world:1.1.0"},
NewVersion: "1.1.0",
CurrentVersion: "1.0.0",
Config: &KeelChartConfig{
Policy: "major",
MatchPreRelease: true,
Trigger: types.TriggerTypePoll,
Images: []ImageDetails{
{RepositoryPath: "image.repository"},
},
Plc: policy.NewSemverPolicy(policy.SemverPolicyTypeMajor, true),
},
},
wantShouldUpdateRelease: true,
wantErr: false,
},
{
name: "no keel config",
args: args{
repo: &types.Repository{Name: "gcr.io/v2-namespace/hello-world", Tag: "1.1.0"},
namespace: "default",
name: "release-1-no-tag",
chart: helloWorldNoKeelCfg,
config: make(map[string]interface{}),
},
wantPlan: &UpdatePlan{Namespace: "default", Name: "release-1-no-tag", Chart: helloWorldNoKeelCfg, Values: map[string]string{}},
wantShouldUpdateRelease: false,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotPlan, gotShouldUpdateRelease, err := checkRelease(tt.args.repo, tt.args.namespace, tt.args.name, tt.args.chart, tt.args.config)
if (err != nil) != tt.wantErr {
t.Errorf("checkRelease() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(gotPlan, tt.wantPlan) {
t.Errorf("checkRelease() gotPlan = %v, want %v", gotPlan, tt.wantPlan)
}
if gotShouldUpdateRelease != tt.wantShouldUpdateRelease {
t.Errorf("checkRelease() gotShouldUpdateRelease = %v, want %v", gotShouldUpdateRelease, tt.wantShouldUpdateRelease)
}
})
}
}

View File

@ -16,7 +16,7 @@ func TestCheckRequestedApproval(t *testing.T) {
fp := &fakeImplementer{}
fp.namespaces = &v1.NamespaceList{
Items: []v1.Namespace{
v1.Namespace{
{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{Name: "xxxx"},
v1.NamespaceSpec{},
@ -37,7 +37,7 @@ func TestCheckRequestedApproval(t *testing.T) {
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1.1",
},
},
@ -88,7 +88,7 @@ func TestCheckRequestedApprovalAnnotation(t *testing.T) {
fp := &fakeImplementer{}
fp.namespaces = &v1.NamespaceList{
Items: []v1.Namespace{
v1.Namespace{
{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{Name: "xxxx"},
v1.NamespaceSpec{},
@ -113,7 +113,7 @@ func TestCheckRequestedApprovalAnnotation(t *testing.T) {
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1.1",
},
},
@ -171,7 +171,7 @@ func TestApprovedCheck(t *testing.T) {
fp := &fakeImplementer{}
fp.namespaces = &v1.NamespaceList{
Items: []v1.Namespace{
v1.Namespace{
{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{Name: "xxxx"},
v1.NamespaceSpec{},
@ -192,7 +192,7 @@ func TestApprovedCheck(t *testing.T) {
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1.1",
},
},
@ -252,7 +252,7 @@ func TestApprovalsCleanup(t *testing.T) {
fp := &fakeImplementer{}
fp.namespaces = &v1.NamespaceList{
Items: []v1.Namespace{
v1.Namespace{
{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{Name: "xxxx"},
v1.NamespaceSpec{},
@ -273,7 +273,7 @@ func TestApprovalsCleanup(t *testing.T) {
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1.1",
},
},

View File

@ -141,7 +141,7 @@ func TestGetNamespaces(t *testing.T) {
fi := &fakeImplementer{
namespaces: &v1.NamespaceList{
Items: []v1.Namespace{
v1.Namespace{
{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{Name: "xxxx"},
v1.NamespaceSpec{},
@ -203,7 +203,7 @@ func TestGetImpacted(t *testing.T) {
fp := &fakeImplementer{}
fp.namespaces = &v1.NamespaceList{
Items: []v1.Namespace{
v1.Namespace{
{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{Name: "xxxx"},
v1.NamespaceSpec{},
@ -224,7 +224,7 @@ func TestGetImpacted(t *testing.T) {
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1.1",
},
},
@ -244,7 +244,7 @@ func TestGetImpacted(t *testing.T) {
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1.1",
},
},
@ -300,7 +300,7 @@ func TestGetImpactedPolicyAnnotations(t *testing.T) {
fp := &fakeImplementer{}
fp.namespaces = &v1.NamespaceList{
Items: []v1.Namespace{
v1.Namespace{
{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{Name: "xxxx"},
v1.NamespaceSpec{},
@ -322,7 +322,7 @@ func TestGetImpactedPolicyAnnotations(t *testing.T) {
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1.1",
},
},
@ -342,7 +342,7 @@ func TestGetImpactedPolicyAnnotations(t *testing.T) {
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1.1",
},
},
@ -403,7 +403,7 @@ func TestPrereleaseGetImpactedA(t *testing.T) {
fp := &fakeImplementer{}
fp.namespaces = &v1.NamespaceList{
Items: []v1.Namespace{
v1.Namespace{
{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{Name: "xxxx"},
v1.NamespaceSpec{},
@ -424,7 +424,7 @@ func TestPrereleaseGetImpactedA(t *testing.T) {
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1.1-staging",
},
},
@ -444,7 +444,7 @@ func TestPrereleaseGetImpactedA(t *testing.T) {
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1.1",
},
},
@ -495,7 +495,7 @@ func TestPrereleaseGetImpactedB(t *testing.T) {
fp := &fakeImplementer{}
fp.namespaces = &v1.NamespaceList{
Items: []v1.Namespace{
v1.Namespace{
{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{Name: "xxxx"},
v1.NamespaceSpec{},
@ -516,7 +516,7 @@ func TestPrereleaseGetImpactedB(t *testing.T) {
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1.1-staging",
},
},
@ -536,7 +536,7 @@ func TestPrereleaseGetImpactedB(t *testing.T) {
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1.1",
},
},
@ -582,7 +582,7 @@ func TestProcessEvent(t *testing.T) {
fp := &fakeImplementer{}
fp.namespaces = &v1.NamespaceList{
Items: []v1.Namespace{
v1.Namespace{
{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{Name: "xxxx"},
v1.NamespaceSpec{},
@ -608,7 +608,7 @@ func TestProcessEvent(t *testing.T) {
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1.1",
},
},
@ -634,7 +634,7 @@ func TestProcessEvent(t *testing.T) {
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/bye-world:1.1.1",
},
},
@ -665,7 +665,7 @@ func TestProcessEvent(t *testing.T) {
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/bye-world:1.1.1",
},
},
@ -710,7 +710,7 @@ func TestProcessEventBuildNumber(t *testing.T) {
fp := &fakeImplementer{}
fp.namespaces = &v1.NamespaceList{
Items: []v1.Namespace{
v1.Namespace{
{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{Name: "xxxx"},
v1.NamespaceSpec{},
@ -731,7 +731,7 @@ func TestProcessEventBuildNumber(t *testing.T) {
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:10",
},
},
@ -773,7 +773,7 @@ func TestEventSent(t *testing.T) {
fp := &fakeImplementer{}
fp.namespaces = &v1.NamespaceList{
Items: []v1.Namespace{
v1.Namespace{
{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{Name: "xxxx"},
v1.NamespaceSpec{},
@ -794,7 +794,7 @@ func TestEventSent(t *testing.T) {
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:10.0.0",
},
},
@ -841,7 +841,7 @@ func TestEventSentWithReleaseNotes(t *testing.T) {
fp := &fakeImplementer{}
fp.namespaces = &v1.NamespaceList{
Items: []v1.Namespace{
v1.Namespace{
{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{Name: "xxxx"},
v1.NamespaceSpec{},
@ -862,7 +862,7 @@ func TestEventSentWithReleaseNotes(t *testing.T) {
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:10.0.0",
},
},
@ -914,7 +914,7 @@ func TestGetImpactedTwoContainersInSameDeployment(t *testing.T) {
fp := &fakeImplementer{}
fp.namespaces = &v1.NamespaceList{
Items: []v1.Namespace{
v1.Namespace{
{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{Name: "xxxx"},
v1.NamespaceSpec{},
@ -935,10 +935,10 @@ func TestGetImpactedTwoContainersInSameDeployment(t *testing.T) {
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1.1",
},
v1.Container{
{
Image: "gcr.io/v2-namespace/greetings-world:1.1.1",
},
},
@ -958,7 +958,7 @@ func TestGetImpactedTwoContainersInSameDeployment(t *testing.T) {
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1.1",
},
},
@ -1015,7 +1015,7 @@ func TestGetImpactedTwoSameContainersInSameDeployment(t *testing.T) {
fp := &fakeImplementer{}
fp.namespaces = &v1.NamespaceList{
Items: []v1.Namespace{
v1.Namespace{
{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{Name: "xxxx"},
v1.NamespaceSpec{},
@ -1036,10 +1036,10 @@ func TestGetImpactedTwoSameContainersInSameDeployment(t *testing.T) {
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1.1",
},
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1.1",
},
},
@ -1060,7 +1060,7 @@ func TestGetImpactedTwoSameContainersInSameDeployment(t *testing.T) {
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1.1",
},
},
@ -1117,7 +1117,7 @@ func TestGetImpactedUntaggedImage(t *testing.T) {
fp := &fakeImplementer{}
fp.namespaces = &v1.NamespaceList{
Items: []v1.Namespace{
v1.Namespace{
{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{Name: "xxxx"},
v1.NamespaceSpec{},
@ -1138,7 +1138,7 @@ func TestGetImpactedUntaggedImage(t *testing.T) {
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/foo-world",
},
},
@ -1159,7 +1159,7 @@ func TestGetImpactedUntaggedImage(t *testing.T) {
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1.1",
},
},
@ -1216,7 +1216,7 @@ func TestGetImpactedUntaggedOneImage(t *testing.T) {
fp := &fakeImplementer{}
fp.namespaces = &v1.NamespaceList{
Items: []v1.Namespace{
v1.Namespace{
{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{Name: "xxxx"},
v1.NamespaceSpec{},
@ -1237,7 +1237,7 @@ func TestGetImpactedUntaggedOneImage(t *testing.T) {
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world",
},
},
@ -1258,7 +1258,7 @@ func TestGetImpactedUntaggedOneImage(t *testing.T) {
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1.1",
},
},
@ -1316,7 +1316,7 @@ func TestTrackedImages(t *testing.T) {
fp := &fakeImplementer{}
fp.namespaces = &v1.NamespaceList{
Items: []v1.Namespace{
v1.Namespace{
{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{Name: "xxxx"},
v1.NamespaceSpec{},
@ -1336,12 +1336,12 @@ func TestTrackedImages(t *testing.T) {
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1",
},
},
ImagePullSecrets: []v1.LocalObjectReference{
v1.LocalObjectReference{
{
Name: "very-secret",
},
},
@ -1380,7 +1380,7 @@ func TestTrackedImagesWithSecrets(t *testing.T) {
fp := &fakeImplementer{}
fp.namespaces = &v1.NamespaceList{
Items: []v1.Namespace{
v1.Namespace{
{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{Name: "xxxx"},
v1.NamespaceSpec{},
@ -1403,12 +1403,12 @@ func TestTrackedImagesWithSecrets(t *testing.T) {
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1",
},
},
ImagePullSecrets: []v1.LocalObjectReference{
v1.LocalObjectReference{
{
Name: "very-secret",
},
},

View File

@ -65,7 +65,7 @@ func TestProvider_checkForUpdate(t *testing.T) {
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world",
},
},
@ -93,7 +93,7 @@ func TestProvider_checkForUpdate(t *testing.T) {
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:latest",
},
},
@ -125,7 +125,7 @@ func TestProvider_checkForUpdate(t *testing.T) {
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/goodbye-world:earliest",
},
},
@ -163,7 +163,7 @@ func TestProvider_checkForUpdate(t *testing.T) {
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:alpha",
},
},
@ -201,7 +201,7 @@ func TestProvider_checkForUpdate(t *testing.T) {
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "karolisr/keel:latest",
},
},
@ -229,7 +229,7 @@ func TestProvider_checkForUpdate(t *testing.T) {
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "karolisr/keel:0.2.0",
},
},
@ -266,7 +266,7 @@ func TestProvider_checkForUpdate(t *testing.T) {
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "karolisr/keel:master",
},
},
@ -296,7 +296,7 @@ func TestProvider_checkForUpdate(t *testing.T) {
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "karolisr/keel:master",
},
},
@ -336,7 +336,7 @@ func TestProvider_checkForUpdate(t *testing.T) {
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "karolisr/keel:latest-staging",
},
},
@ -366,7 +366,7 @@ func TestProvider_checkForUpdate(t *testing.T) {
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "karolisr/keel:latest-staging",
},
},
@ -406,7 +406,7 @@ func TestProvider_checkForUpdate(t *testing.T) {
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "eu.gcr.io/karolisr/keel:latest-staging",
},
},
@ -437,7 +437,7 @@ func TestProvider_checkForUpdate(t *testing.T) {
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "eu.gcr.io/karolisr/keel:latest-staging",
},
},
@ -471,7 +471,7 @@ func TestProvider_checkForUpdate(t *testing.T) {
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "karolisr/keel:latest-acceptance",
},
},
@ -511,7 +511,7 @@ func TestProvider_checkForUpdate(t *testing.T) {
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "eu.gcr.io/karolisr/keel:latest-staging",
},
},
@ -542,7 +542,7 @@ func TestProvider_checkForUpdate(t *testing.T) {
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "eu.gcr.io/karolisr/keel:latest-staging",
},
},
@ -581,7 +581,7 @@ func TestProvider_checkForUpdate(t *testing.T) {
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "eu.gcr.io/karolisr/keel:release-1",
},
},
@ -612,7 +612,7 @@ func TestProvider_checkForUpdate(t *testing.T) {
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "eu.gcr.io/karolisr/keel:release-2",
},
},
@ -693,7 +693,7 @@ func TestProvider_checkForUpdateSemver(t *testing.T) {
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1.1",
},
},
@ -721,7 +721,7 @@ func TestProvider_checkForUpdateSemver(t *testing.T) {
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1.2",
},
},
@ -759,7 +759,7 @@ func TestProvider_checkForUpdateSemver(t *testing.T) {
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-prerelease:v1.1.1",
},
},
@ -796,7 +796,7 @@ func TestProvider_checkForUpdateSemver(t *testing.T) {
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-prerelease:v1.1.1-staging",
},
},
@ -828,7 +828,7 @@ func TestProvider_checkForUpdateSemver(t *testing.T) {
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1.1",
},
},
@ -868,10 +868,10 @@ func TestProvider_checkForUpdateSemver(t *testing.T) {
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1.1",
},
v1.Container{
{
Image: "yo-world:1.1.1",
},
},
@ -899,10 +899,10 @@ func TestProvider_checkForUpdateSemver(t *testing.T) {
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1.2",
},
v1.Container{
{
Image: "yo-world:1.1.1",
},
},
@ -939,10 +939,10 @@ func TestProvider_checkForUpdateSemver(t *testing.T) {
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:latest",
},
v1.Container{
{
Image: "yo-world:1.1.1",
},
},
@ -970,10 +970,10 @@ func TestProvider_checkForUpdateSemver(t *testing.T) {
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1.2",
},
v1.Container{
{
Image: "yo-world:1.1.1",
},
},
@ -1013,10 +1013,10 @@ func TestProvider_checkForUpdateSemver(t *testing.T) {
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1.2",
},
v1.Container{
{
Image: "yo-world:1.1.1",
},
},
@ -1047,10 +1047,10 @@ func TestProvider_checkForUpdateSemver(t *testing.T) {
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1.2",
},
v1.Container{
{
Image: "yo-world:1.1.1",
},
},
@ -1090,10 +1090,10 @@ func TestProvider_checkForUpdateSemver(t *testing.T) {
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1.2",
},
v1.Container{
{
Image: "yo-world:1.1.1",
},
},

View File

@ -119,6 +119,12 @@ If you work mostly with regular Kubernetes manifests, you can install Keel witho
helm upgrade --install keel --namespace=keel keel/keel --set helmProvider.enabled="false"
```
To install for Helm v3, set helmProvider.version="v3" (default is "v2"):
```bash
helm install keel keel/keel --set helmProvider.version="v3"
```
That's it, see [Configuration](https://github.com/keel-hq/keel#configuration) section now.
### Quick Start

View File

@ -305,14 +305,11 @@ var tagsResp = `{
]
}`
func TestGetDockerHubManyTags(t *testing.T) {
client := registry.New("https://quay.io", "", "")
tags, err := client.Tags("coreos/prometheus-operator")
if err != nil {
t.Errorf("error while getting repo: %s", err)
}
fmt.Println(tags)
func TestGetDockerHubManyTags(t *testing.T) {
client := registry.New("https://quay.io", "", "")
tags, err := client.Tags("coreos/prometheus-operator")
if err != nil {
t.Errorf("error while getting repo: %s", err)
}
fmt.Println(tags)
}

View File

@ -35,7 +35,7 @@ func TestGetSecret(t *testing.T) {
impl := &testutil.FakeK8sImplementer{
AvailableSecret: map[string]*v1.Secret{
"myregistrysecret": &v1.Secret{
"myregistrysecret": {
Data: map[string][]byte{
dockerConfigKey: []byte(secretDataPayload),
},
@ -71,7 +71,7 @@ func TestGetDockerConfigJSONSecret(t *testing.T) {
impl := &testutil.FakeK8sImplementer{
AvailableSecret: map[string]*v1.Secret{
"myregistrysecret": &v1.Secret{
"myregistrysecret": {
Data: map[string][]byte{
dockerConfigJSONKey: []byte(secretDockerConfigJSONPayload),
},
@ -106,7 +106,7 @@ func TestGetDockerConfigJSONSecretUsernmePassword(t *testing.T) {
impl := &testutil.FakeK8sImplementer{
AvailableSecret: map[string]*v1.Secret{
"myregistrysecret": &v1.Secret{
"myregistrysecret": {
Data: map[string][]byte{
dockerConfigJSONKey: []byte(secretDockerConfigJSONPayloadWithUsernamePassword),
},
@ -142,7 +142,7 @@ func TestGetFromDefaultCredentials(t *testing.T) {
impl := &testutil.FakeK8sImplementer{
AvailableSecret: map[string]*v1.Secret{
"myregistrysecret": &v1.Secret{
"myregistrysecret": {
Data: map[string][]byte{
dockerConfigJSONKey: []byte(secretDockerConfigJSONPayloadWithUsernamePassword),
},
@ -215,9 +215,9 @@ func TestLookupHelmSecret(t *testing.T) {
impl := &testutil.FakeK8sImplementer{
AvailablePods: &v1.PodList{
Items: []v1.Pod{
v1.Pod{
{
Spec: v1.PodSpec{ImagePullSecrets: []v1.LocalObjectReference{
v1.LocalObjectReference{
{
Name: "very-secret",
},
},
@ -226,7 +226,7 @@ func TestLookupHelmSecret(t *testing.T) {
},
},
AvailableSecret: map[string]*v1.Secret{
"myregistrysecret": &v1.Secret{
"myregistrysecret": {
Data: map[string][]byte{
dockerConfigKey: []byte(fmt.Sprintf(secretDataPayloadEncoded, mustEncode("user-y:pass-y"))),
},
@ -263,9 +263,9 @@ func TestLookupHelmEncodedSecret(t *testing.T) {
impl := &testutil.FakeK8sImplementer{
AvailablePods: &v1.PodList{
Items: []v1.Pod{
v1.Pod{
{
Spec: v1.PodSpec{ImagePullSecrets: []v1.LocalObjectReference{
v1.LocalObjectReference{
{
Name: "very-secret",
},
},
@ -274,7 +274,7 @@ func TestLookupHelmEncodedSecret(t *testing.T) {
},
},
AvailableSecret: map[string]*v1.Secret{
"myregistrysecret": &v1.Secret{
"myregistrysecret": {
Data: map[string][]byte{
dockerConfigKey: []byte(secretDataPayload),
},
@ -311,10 +311,10 @@ func TestGetDirectHelmSecret(t *testing.T) {
impl := &testutil.FakeK8sImplementer{
AvailablePods: &v1.PodList{
Items: []v1.Pod{
v1.Pod{
{
Spec: v1.PodSpec{
ImagePullSecrets: []v1.LocalObjectReference{
v1.LocalObjectReference{
{
Name: "very-secret-dont-look",
},
},
@ -323,13 +323,13 @@ func TestGetDirectHelmSecret(t *testing.T) {
},
},
AvailableSecret: map[string]*v1.Secret{
"myregistrysecret": &v1.Secret{
"myregistrysecret": {
Data: map[string][]byte{
dockerConfigKey: []byte(secretDataPayload2),
},
Type: v1.SecretTypeDockercfg,
},
"very-secret-dont-look": &v1.Secret{
"very-secret-dont-look": {
Data: map[string][]byte{
dockerConfigKey: []byte(secretDataPayload),
},
@ -367,9 +367,9 @@ func TestLookupHelmNoSecretsFound(t *testing.T) {
impl := &testutil.FakeK8sImplementer{
AvailablePods: &v1.PodList{
Items: []v1.Pod{
v1.Pod{
{
Spec: v1.PodSpec{ImagePullSecrets: []v1.LocalObjectReference{
v1.LocalObjectReference{
{
Name: "very-secret",
},
},
@ -411,9 +411,9 @@ func TestLookupWithPortedRegistry(t *testing.T) {
impl := &testutil.FakeK8sImplementer{
AvailablePods: &v1.PodList{
Items: []v1.Pod{
v1.Pod{
{
Spec: v1.PodSpec{ImagePullSecrets: []v1.LocalObjectReference{
v1.LocalObjectReference{
{
Name: "example.com",
},
},
@ -422,7 +422,7 @@ func TestLookupWithPortedRegistry(t *testing.T) {
},
},
AvailableSecret: map[string]*v1.Secret{
"example.com": &v1.Secret{
"example.com": {
Data: map[string][]byte{
dockerConfigKey: []byte(secretDataPayloadWithPort),
},

View File

@ -80,7 +80,7 @@ func TestPollingSemverUpdate(t *testing.T) {
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Name: "wd-1",
Image: "keelhq/push-workflow-example:0.1.0-dev",
},
@ -140,7 +140,7 @@ func TestPollingSemverUpdate(t *testing.T) {
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Name: "wd-1",
Image: "keelhq/push-workflow-example:0.1.0",
},
@ -197,7 +197,7 @@ func TestPollingSemverUpdate(t *testing.T) {
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Name: "wd-1",
Image: "keelhq/push-workflow-example:0.3.0-alpha",
},
@ -335,7 +335,7 @@ func TestPollingPrivateRegistry(t *testing.T) {
},
},
Containers: []v1.Container{
v1.Container{
{
ImagePullPolicy: v1.PullAlways,
Name: "wd-1",
Image: "karolisr/demo-webhook:0.0.1",
@ -447,7 +447,7 @@ func TestPollingPrivateRegistry(t *testing.T) {
},
},
Containers: []v1.Container{
v1.Container{
{
ImagePullPolicy: v1.PullAlways,
Name: "wd-1",
Image: "registry.gitlab.com/karolisr/keel:0.1.0",

View File

@ -101,7 +101,7 @@ func TestWebhooksSemverUpdate(t *testing.T) {
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Name: "wd-1",
Image: "karolisr/webhook-demo:0.0.14",
},
@ -205,7 +205,7 @@ func TestWebhookHighIntegerUpdate(t *testing.T) {
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Name: "wd-1",
Image: "karolisr/webhook-demo:0.0.14",
},
@ -278,6 +278,10 @@ func TestWebhookHighIntegerUpdate(t *testing.T) {
func TestApprovals(t *testing.T) {
// approvals endpoint shouldn't work without at least basic auth
// see http.go:134
t.Skip()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
@ -335,7 +339,7 @@ func TestApprovals(t *testing.T) {
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Name: "wd-1",
Image: "karolisr/webhook-demo:0.0.14",
},
@ -476,7 +480,7 @@ func TestApprovalsWithAuthentication(t *testing.T) {
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
{
Name: "wd-1",
Image: "karolisr/webhook-demo:0.0.14",
},
@ -537,6 +541,10 @@ func TestApprovalsWithAuthentication(t *testing.T) {
t.Errorf("failed to make req: %s", err)
}
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected %d, got: %d", http.StatusOK, resp.StatusCode)
}
var approvals []*types.Approval
dec := json.NewDecoder(resp.Body)
defer resp.Body.Close()

View File

@ -55,14 +55,14 @@ func TestCheckDeployment(t *testing.T) {
fp := &fakeProvider{
images: []*types.TrackedImage{
&types.TrackedImage{
{
Image: imgA,
Trigger: types.TriggerTypePoll,
Provider: "fp",
PollSchedule: types.KeelPollDefaultSchedule,
},
&types.TrackedImage{
{
Trigger: types.TriggerTypePoll,
Image: imgB,
Provider: "fp",
@ -144,7 +144,7 @@ func TestCheckECRDeployment(t *testing.T) {
imgA, _ := image.Parse("528670773427.dkr.ecr.us-east-2.amazonaws.com/webhook-demo:master")
fp := &fakeProvider{
images: []*types.TrackedImage{
&types.TrackedImage{
{
Image: imgA,
Trigger: types.TriggerTypePoll,
Provider: "fp",

View File

@ -23,7 +23,7 @@ func TestWatchMultipleTagsWithSemver(t *testing.T) {
imgA, _ := image.Parse("gcr.io/v2-namespace/hello-world:1.1.1")
fp := &fakeProvider{
images: []*types.TrackedImage{
&types.TrackedImage{
{
Image: imgA,
Trigger: types.TriggerTypePoll,
Provider: "fp",
@ -227,7 +227,7 @@ func TestWatchMultipleTagsWithCredentialsHelper(t *testing.T) {
imgA, _ := image.Parse("gcr.io/v2-namespace/hello-world:1.1.1")
fp := &fakeProvider{
images: []*types.TrackedImage{
&types.TrackedImage{
{
Image: imgA,
Trigger: types.TriggerTypePoll,
Provider: "fp",

View File

@ -177,7 +177,7 @@ func TestWatchAllTagsJob(t *testing.T) {
reference, _ := image.Parse("foo/bar:1.1.0")
fp := &fakeProvider{
images: []*types.TrackedImage{
&types.TrackedImage{
{
Image: reference,
Policy: policy.NewSemverPolicy(policy.SemverPolicyTypeAll, true),
},
@ -221,7 +221,7 @@ func TestWatchAllTagsJobCurrentLatest(t *testing.T) {
reference, _ := image.Parse("foo/bar:latest")
fp := &fakeProvider{
images: []*types.TrackedImage{
&types.TrackedImage{
{
Image: reference,
Policy: policy.NewForcePolicy(true),
},
@ -264,7 +264,7 @@ func TestWatchMultipleTags(t *testing.T) {
fp := &fakeProvider{
images: []*types.TrackedImage{
&types.TrackedImage{
{
Image: imgA,
Trigger: types.TriggerTypePoll,
Provider: "fp",
@ -273,7 +273,7 @@ func TestWatchMultipleTags(t *testing.T) {
Policy: policy.NewSemverPolicy(policy.SemverPolicyTypeMajor, true),
},
&types.TrackedImage{
{
Trigger: types.TriggerTypePoll,
Image: imgB,
Provider: "fp",
@ -281,7 +281,7 @@ func TestWatchMultipleTags(t *testing.T) {
Policy: policy.NewSemverPolicy(policy.SemverPolicyTypeMajor, true),
},
&types.TrackedImage{
{
Trigger: types.TriggerTypePoll,
Image: imgC,
Provider: "fp",
@ -289,7 +289,7 @@ func TestWatchMultipleTags(t *testing.T) {
Policy: policy.NewForcePolicy(true),
},
&types.TrackedImage{
{
Trigger: types.TriggerTypePoll,
Image: imgD,
Provider: "fp",
@ -422,7 +422,7 @@ func TestWatchTagJobLatestECR(t *testing.T) {
imgA, _ := image.Parse("528670773427.dkr.ecr.us-east-2.amazonaws.com/webhook-demo:master")
fp := &fakeProvider{
images: []*types.TrackedImage{
&types.TrackedImage{
{
Image: imgA,
Trigger: types.TriggerTypePoll,
Provider: "fp",

View File

@ -82,7 +82,7 @@ func TestCheckDeployment(t *testing.T) {
img, _ := image.Parse("gcr.io/v2-namespace/hello-world:1.1")
fp := &fakeProvider{
images: []*types.TrackedImage{
&types.TrackedImage{
{
Image: img,
Provider: "fp",
},

View File

@ -13,4 +13,4 @@ func ValidateID(id string) error {
return fmt.Errorf("image ID '%s' is invalid ", id)
}
return nil
}
}