chore: remove some duplication and add a first few tests

This commit is contained in:
2022-09-30 22:07:56 +02:00
parent e124a2ed6b
commit 762fa4f747
8 changed files with 542 additions and 70 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ func main() {
if kubecfg, exists := os.LookupEnv("KUBECONFIG"); exists { if kubecfg, exists := os.LookupEnv("KUBECONFIG"); exists {
kubeClient = kube.New(kube.WithKubeConfigProvider(kubecfg)) kubeClient = kube.New(kube.WithKubeConfigProvider(kubecfg))
} else { } else {
kubeClient = kube.New(kube.WithInClusterProvider()) kubeClient = kube.New()
} }
gitlabClient := gitlab.New(cli.GitlabToken) gitlabClient := gitlab.New(cli.GitlabToken)
if err := handle(cli, logger, kubeClient, gitlabClient); err != nil { if err := handle(cli, logger, kubeClient, gitlabClient); err != nil {
+19 -26
View File
@@ -12,20 +12,16 @@ import (
) )
func New(token string) *RestClient { func New(token string) *RestClient {
return &RestClient{token: token, client: http.DefaultClient} return &RestClient{token: token, client: http.DefaultClient, baseUrl: "https://gitlab.com"}
} }
type RestClient struct { type RestClient struct {
client *http.Client client *http.Client
token string token string
baseUrl string
} }
func (r *RestClient) UpdateCleanupPolicy(project string, versions []string) error { func (r *RestClient) UpdateCleanupPolicy(project string, versions []string) error {
encoded := url.QueryEscape(project)
reqUrl, err := url.Parse(fmt.Sprintf("https://gitlab.com/api/v4/projects/%s", encoded))
if err != nil {
return err
}
options := ProjectConfig{ options := ProjectConfig{
ContainerExpirationPolicyAttributes: ContainerExpirationPolicyAttributes{ ContainerExpirationPolicyAttributes: ContainerExpirationPolicyAttributes{
Cadence: "1d", Cadence: "1d",
@@ -38,46 +34,43 @@ func (r *RestClient) UpdateCleanupPolicy(project string, versions []string) erro
} }
buff := &bytes.Buffer{} buff := &bytes.Buffer{}
encoder := json.NewEncoder(buff) encoder := json.NewEncoder(buff)
err = encoder.Encode(&options) err := encoder.Encode(&options)
if err != nil { if err != nil {
return err return err
} }
header := http.Header{} return r.projectApiCall("PUT", project, "", io.NopCloser(buff), nil)
header.Add("Content-Type", "application/json;charset=UTF-8")
header.Add("PRIVATE-TOKEN", r.token)
req := &http.Request{
Method: "PUT",
URL: reqUrl,
Header: header,
Body: io.NopCloser(buff),
}
_, err = r.client.Do(req)
return err
} }
func (r *RestClient) GetTags(project string) ([]Tag, error) { func (r *RestClient) GetTags(project string) ([]Tag, error) {
var tags []Tag
err := r.projectApiCall("GET", project, "/repository/tags", nil, &tags)
return tags, err
}
func (r *RestClient) projectApiCall(method, project string, api string, body io.ReadCloser, response interface{}) error {
encoded := url.QueryEscape(project) encoded := url.QueryEscape(project)
reqUrl, err := url.Parse(fmt.Sprintf("https://gitlab.com/api/v4/projects/%s/repository/tags", encoded)) reqUrl, err := url.Parse(fmt.Sprintf("%s/api/v4/projects/%s%s", r.baseUrl, encoded, api))
if err != nil { if err != nil {
return nil, err return err
} }
header := http.Header{} header := http.Header{}
header.Add("Content-Type", "application/json;charset=UTF-8") header.Add("Content-Type", "application/json;charset=UTF-8")
header.Add("PRIVATE-TOKEN", r.token) header.Add("PRIVATE-TOKEN", r.token)
req := &http.Request{ req := &http.Request{
Method: "GET", Method: method,
URL: reqUrl, URL: reqUrl,
Header: header, Header: header,
Body: body,
} }
resp, err := r.client.Do(req) resp, err := r.client.Do(req)
if err != nil { if err != nil {
return nil, err return err
} }
if resp.StatusCode == http.StatusOK && response != nil {
var tags []Tag
decoder := json.NewDecoder(resp.Body) decoder := json.NewDecoder(resp.Body)
err = decoder.Decode(&tags) err = decoder.Decode(response)
return tags, err }
return err
} }
type ProjectConfig struct { type ProjectConfig struct {
+118
View File
@@ -0,0 +1,118 @@
package gitlab
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestRestClient_UpdateCleanupPolicy(t *testing.T) {
type args struct {
project string
versions []string
}
tests := []struct {
name string
args args
handler func(t *testing.T) http.HandlerFunc
wantErr assert.ErrorAssertionFunc
}{
{
name: "success",
args: args{
project: "unboundsoftware/dummy",
versions: []string{"1.0", "1.1"},
},
handler: func(t *testing.T) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
buff, err := io.ReadAll(request.Body)
assert.NoError(t, err)
assert.Equal(t, "{\"container_expiration_policy_attributes\":{\"cadence\":\"1d\",\"enabled\":true,\"keep_n\":10,\"older_than\":\"14d\",\"name_regex\":\".*\",\"name_regex_keep\":\"(main|master|1.0|1.1)\"}}\n", string(buff))
writer.WriteHeader(http.StatusOK)
}
},
wantErr: assert.NoError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := httptest.NewServer(tt.handler(t))
defer server.Close()
r := &RestClient{
client: http.DefaultClient,
token: "some-gitlab-token",
baseUrl: server.URL,
}
tt.wantErr(t, r.UpdateCleanupPolicy(tt.args.project, tt.args.versions), fmt.Sprintf("UpdateCleanupPolicy(%v, %v)", tt.args.project, tt.args.versions))
})
}
}
func TestRestClient_GetTags(t *testing.T) {
type args struct {
project string
}
tests := []struct {
name string
args args
handler func(t *testing.T) http.HandlerFunc
want []Tag
wantErr assert.ErrorAssertionFunc
}{
{
name: "error",
args: args{
project: "unboundsoftware/dummy",
},
handler: func(t *testing.T) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Content-Length", "23")
writer.WriteHeader(http.StatusOK)
_, _ = writer.Write([]byte("abc"))
}
},
want: nil,
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
return assert.EqualError(t, err, "invalid character 'a' looking for beginning of value")
},
},
{
name: "success",
args: args{
project: "unboundsoftware/dummy",
},
handler: func(t *testing.T) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
//writer.Header().Set("Content-Length", "23")
writer.WriteHeader(http.StatusOK)
_, _ = writer.Write([]byte(`[{"name":"1.0"},{"name": "1.1"}]`))
}
},
want: []Tag{
{Name: "1.0"},
{Name: "1.1"},
},
wantErr: assert.NoError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := httptest.NewServer(tt.handler(t))
defer server.Close()
r := &RestClient{
client: http.DefaultClient,
token: "some-gitlab-token",
baseUrl: server.URL,
}
got, err := r.GetTags(tt.args.project)
if !tt.wantErr(t, err, fmt.Sprintf("GetTags(%v)", tt.args.project)) {
return
}
assert.Equalf(t, tt.want, got, "GetTags(%v)", tt.args.project)
})
}
}
+3
View File
@@ -5,6 +5,8 @@ go 1.19
require ( require (
github.com/alecthomas/kong v0.6.1 github.com/alecthomas/kong v0.6.1
github.com/apex/log v1.9.0 github.com/apex/log v1.9.0
github.com/stretchr/testify v1.7.2
gitlab.com/unboundsoftware/apex-mocks v0.2.0
k8s.io/apimachinery v0.25.2 k8s.io/apimachinery v0.25.2
k8s.io/client-go v0.25.2 k8s.io/client-go v0.25.2
) )
@@ -30,6 +32,7 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
+2
View File
@@ -221,6 +221,8 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
gitlab.com/unboundsoftware/apex-mocks v0.2.0 h1:IFt+uyIoOkSl4qdUBLUSIvOhaRdQRGB6TnpZqfRuXqY=
gitlab.com/unboundsoftware/apex-mocks v0.2.0/go.mod h1:FGsQjCu/nS6b+QaBpAFvms6p0Chr0aobGcUPeeZNSNo=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+30
View File
@@ -0,0 +1,30 @@
package kube
import "strings"
type ImageCollector map[string]map[string]struct{}
func NewImageCollector() ImageCollector {
return make(map[string]map[string]struct{})
}
func (c *ImageCollector) Add(image string) {
parts := strings.Split(image[20:], ":")
if x, exists := (*c)[parts[0]]; exists {
x[parts[1]] = struct{}{}
} else {
(*c)[parts[0]] = map[string]struct{}{
parts[1]: {},
}
}
}
func (c *ImageCollector) Images() map[string][]string {
images := make(map[string][]string)
for i, x := range *c {
for v := range x {
images[i] = append(images[i], v)
}
}
return images
}
+1 -34
View File
@@ -19,12 +19,6 @@ type Client struct {
type Options func(*Client) type Options func(*Client)
func WithInClusterProvider() func(c *Client) {
return func(c *Client) {
c.provider = &DefaultProvider{provider: &InClusterProvider{}}
}
}
func WithKubeConfigProvider(kubeconfig string) func(c *Client) { func WithKubeConfigProvider(kubeconfig string) func(c *Client) {
return func(c *Client) { return func(c *Client) {
c.provider = &DefaultProvider{provider: &PathConfigProvider{kubecfg: kubeconfig}} c.provider = &DefaultProvider{provider: &PathConfigProvider{kubecfg: kubeconfig}}
@@ -32,7 +26,7 @@ func WithKubeConfigProvider(kubeconfig string) func(c *Client) {
} }
func New(opts ...Options) *Client { func New(opts ...Options) *Client {
c := &Client{} c := &Client{provider: &DefaultProvider{provider: &InClusterProvider{}}}
for _, opt := range opts { for _, opt := range opts {
opt(c) opt(c)
} }
@@ -119,30 +113,3 @@ func (k PathConfigProvider) Provide() (*rest.Config, error) {
} }
var _ ConfigProvider = &PathConfigProvider{} var _ ConfigProvider = &PathConfigProvider{}
type ImageCollector map[string]map[string]struct{}
func NewImageCollector() ImageCollector {
return make(map[string]map[string]struct{})
}
func (c *ImageCollector) Add(image string) {
parts := strings.Split(image[20:], ":")
if x, exists := (*c)[parts[0]]; exists {
x[parts[1]] = struct{}{}
} else {
(*c)[parts[0]] = map[string]struct{}{
parts[1]: {},
}
}
}
func (c *ImageCollector) Images() map[string][]string {
images := make(map[string][]string)
for i, x := range *c {
for v := range x {
images[i] = append(images[i], v)
}
}
return images
}
+359
View File
@@ -0,0 +1,359 @@
package kube
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"gitlab.com/unboundsoftware/apex-mocks"
v1 "k8s.io/api/apps/v1"
batchapiv1 "k8s.io/api/batch/v1"
v12 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
appsv1 "k8s.io/client-go/kubernetes/typed/apps/v1"
batchv1 "k8s.io/client-go/kubernetes/typed/batch/v1"
)
func TestClient_GetImages(t *testing.T) {
type fields struct {
provider ClientProvider
}
type args struct {
namespaces []string
}
tests := []struct {
name string
fields fields
args args
want map[string][]string
wantLogged []string
wantErr assert.ErrorAssertionFunc
}{
{
name: "error getting client",
fields: fields{
provider: MockClientProvider(func() (APIClient, error) {
return nil, fmt.Errorf("error")
}),
},
args: args{},
want: nil,
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
return assert.EqualError(t, err, "error")
},
},
{
name: "error fetching deployments",
fields: fields{
provider: MockClientProvider(func() (APIClient, error) {
return &MockAPIClient{
Apps: func() appsv1.AppsV1Interface {
return &MockApps{
DeploymentsFn: func(namespace string) appsv1.DeploymentInterface {
assert.Equal(t, "default", namespace)
return &MockDeployments{
ListFn: func(ctx context.Context, opts metav1.ListOptions) (*v1.DeploymentList, error) {
return nil, fmt.Errorf("error")
},
}
},
}
},
}, nil
}),
},
args: args{
namespaces: []string{"default"},
},
want: nil,
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
return assert.EqualError(t, err, "error")
},
},
{
name: "error fetching cron jobs",
fields: fields{
provider: MockClientProvider(func() (APIClient, error) {
return &MockAPIClient{
Apps: func() appsv1.AppsV1Interface {
return &MockApps{
DeploymentsFn: func(namespace string) appsv1.DeploymentInterface {
return &MockDeployments{
ListFn: func(ctx context.Context, opts metav1.ListOptions) (*v1.DeploymentList, error) {
return &v1.DeploymentList{}, nil
},
}
},
}
},
Batch: func() batchv1.BatchV1Interface {
return &MockBatch{
CronJobsFn: func(namespace string) batchv1.CronJobInterface {
assert.Equal(t, "default", namespace)
return &MockCronJobs{
ListFn: func(ctx context.Context, opts metav1.ListOptions) (*batchapiv1.CronJobList, error) {
return nil, fmt.Errorf("error")
},
}
},
}
},
}, nil
}),
},
args: args{
namespaces: []string{"default"},
},
want: nil,
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
return assert.EqualError(t, err, "error")
},
},
{
name: "no deployments or cronjobs",
fields: fields{
provider: MockClientProvider(func() (APIClient, error) {
return &MockAPIClient{
Apps: func() appsv1.AppsV1Interface {
return &MockApps{
DeploymentsFn: func(namespace string) appsv1.DeploymentInterface {
return &MockDeployments{
ListFn: func(ctx context.Context, opts metav1.ListOptions) (*v1.DeploymentList, error) {
return &v1.DeploymentList{}, nil
},
}
},
}
},
Batch: func() batchv1.BatchV1Interface {
return &MockBatch{
CronJobsFn: func(namespace string) batchv1.CronJobInterface {
return &MockCronJobs{
ListFn: func(ctx context.Context, opts metav1.ListOptions) (*batchapiv1.CronJobList, error) {
return &batchapiv1.CronJobList{}, nil
},
}
},
}
},
}, nil
}),
},
args: args{
namespaces: []string{"default"},
},
want: map[string][]string{},
wantErr: assert.NoError,
},
{
name: "deployments and cronjobs in multiple namespaces",
fields: fields{
provider: MockClientProvider(func() (APIClient, error) {
return &MockAPIClient{
Apps: func() appsv1.AppsV1Interface {
return &MockApps{
DeploymentsFn: func(namespace string) appsv1.DeploymentInterface {
return &MockDeployments{
ListFn: func(ctx context.Context, opts metav1.ListOptions) (*v1.DeploymentList, error) {
if namespace == "default" {
return &v1.DeploymentList{
Items: []v1.Deployment{
{
ObjectMeta: metav1.ObjectMeta{
Name: "some-deployment",
},
Spec: v1.DeploymentSpec{
Template: v12.PodTemplateSpec{
Spec: v12.PodSpec{
Containers: []v12.Container{
{
Image: "registry.gitlab.com/unboundsoftware/dummy:abc123",
},
},
},
},
},
},
},
}, nil
}
return &v1.DeploymentList{
Items: []v1.Deployment{
{
ObjectMeta: metav1.ObjectMeta{
Name: "other-deployment",
},
Spec: v1.DeploymentSpec{
Template: v12.PodTemplateSpec{
Spec: v12.PodSpec{
Containers: []v12.Container{
{
Image: "registry.gitlab.com/unboundsoftware/dummy:def456",
},
},
},
},
},
},
},
}, nil
},
}
},
}
},
Batch: func() batchv1.BatchV1Interface {
return &MockBatch{
CronJobsFn: func(namespace string) batchv1.CronJobInterface {
return &MockCronJobs{
ListFn: func(ctx context.Context, opts metav1.ListOptions) (*batchapiv1.CronJobList, error) {
if namespace == "other" {
return &batchapiv1.CronJobList{
Items: []batchapiv1.CronJob{
{
ObjectMeta: metav1.ObjectMeta{
Name: "some-cronjob",
},
Spec: batchapiv1.CronJobSpec{
JobTemplate: batchapiv1.JobTemplateSpec{
Spec: batchapiv1.JobSpec{
Template: v12.PodTemplateSpec{
Spec: v12.PodSpec{
Containers: []v12.Container{
{
Image: "registry.gitlab.com/unboundsoftware/other:xxx111",
},
},
},
},
},
},
},
},
},
}, nil
}
return &batchapiv1.CronJobList{}, nil
},
}
},
}
},
}, nil
}),
},
args: args{
namespaces: []string{"default", "other"},
},
want: map[string][]string{
"unboundsoftware/dummy": {"abc123", "def456"},
"unboundsoftware/other": {"xxx111"},
},
wantLogged: []string{
"info: Found image 'unboundsoftware/dummy:abc123' in deployment default.some-deployment",
"info: Found image 'unboundsoftware/dummy:def456' in deployment other.other-deployment",
"info: Found image 'unboundsoftware/other:xxx111' in cronjob other.some-cronjob",
},
wantErr: assert.NoError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Client{
provider: tt.fields.provider,
}
logger := apex.New()
ctx := context.Background()
got, err := c.GetImages(ctx, logger, tt.args.namespaces)
if !tt.wantErr(t, err, fmt.Sprintf("GetImages(%v, %v, %v)", ctx, logger, tt.args.namespaces)) {
return
}
assert.Equalf(t, tt.want, got, "GetImages(%v, %v, %v)", ctx, logger, tt.args.namespaces)
logger.Check(t, tt.wantLogged)
})
}
}
type MockClientProvider func() (APIClient, error)
func (m MockClientProvider) Provide() (APIClient, error) {
return m()
}
type MockAPIClient struct {
Apps func() appsv1.AppsV1Interface
Batch func() batchv1.BatchV1Interface
}
func (m *MockAPIClient) AppsV1() appsv1.AppsV1Interface {
if m.Apps == nil {
return nil
}
return m.Apps()
}
func (m *MockAPIClient) BatchV1() batchv1.BatchV1Interface {
if m.Batch == nil {
return nil
}
return m.Batch()
}
var _ APIClient = &MockAPIClient{}
type MockApps struct {
appsv1.AppsV1Interface
DeploymentsFn func(namespace string) appsv1.DeploymentInterface
}
func (a *MockApps) Deployments(namespace string) appsv1.DeploymentInterface {
if a.DeploymentsFn == nil {
return nil
}
return a.DeploymentsFn(namespace)
}
var _ appsv1.AppsV1Interface = &MockApps{}
type MockDeployments struct {
appsv1.DeploymentInterface
ListFn func(ctx context.Context, opts metav1.ListOptions) (*v1.DeploymentList, error)
}
func (d *MockDeployments) List(ctx context.Context, opts metav1.ListOptions) (*v1.DeploymentList, error) {
if d.ListFn == nil {
return nil, nil
}
return d.ListFn(ctx, opts)
}
var _ appsv1.DeploymentInterface = &MockDeployments{}
type MockBatch struct {
batchv1.BatchV1Interface
CronJobsFn func(namespace string) batchv1.CronJobInterface
}
func (b *MockBatch) CronJobs(namespace string) batchv1.CronJobInterface {
if b.CronJobsFn == nil {
return nil
}
return b.CronJobsFn(namespace)
}
var _ batchv1.BatchV1Interface = &MockBatch{}
type MockCronJobs struct {
batchv1.CronJobInterface
ListFn func(ctx context.Context, opts metav1.ListOptions) (*batchapiv1.CronJobList, error)
}
func (m *MockCronJobs) List(ctx context.Context, opts metav1.ListOptions) (*batchapiv1.CronJobList, error) {
if m.ListFn == nil {
return nil, nil
}
return m.ListFn(ctx, opts)
}
var _ batchv1.CronJobInterface = &MockCronJobs{}