diff --git a/go.mod b/go.mod index 2c5b0f5..bf146ed 100644 --- a/go.mod +++ b/go.mod @@ -15,12 +15,12 @@ require ( github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 github.com/pterm/pterm v0.12.79 golang.org/x/mod v0.17.0 - gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 helm.sh/helm/v3 v3.14.2 k8s.io/api v0.29.2 k8s.io/apimachinery v0.29.2 k8s.io/client-go v0.29.2 + k8s.io/kubectl v0.29.0 sigs.k8s.io/kind v0.24.0 ) @@ -158,13 +158,13 @@ require ( google.golang.org/protobuf v1.33.0 // indirect gopkg.in/evanphx/json-patch.v5 v5.7.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect k8s.io/apiextensions-apiserver v0.29.0 // indirect k8s.io/apiserver v0.29.0 // indirect k8s.io/cli-runtime v0.29.0 // indirect k8s.io/component-base v0.29.0 // indirect k8s.io/klog/v2 v2.110.1 // indirect k8s.io/kube-openapi v0.0.0-20240103195357-a9f8850cb432 // indirect - k8s.io/kubectl v0.29.0 // indirect k8s.io/utils v0.0.0-20240102154912-e7106e64919e // indirect oras.land/oras-go v1.2.5 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index ca17aba..c819b08 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -2,8 +2,8 @@ package cmd import ( "fmt" - "os" + "github.com/airbytehq/abctl/internal/cmd/images" "github.com/airbytehq/abctl/internal/cmd/local" "github.com/airbytehq/abctl/internal/cmd/local/k8s" "github.com/airbytehq/abctl/internal/cmd/local/localerr" @@ -24,14 +24,12 @@ func (v verbose) BeforeApply() error { type Cmd struct { Local local.Cmd `cmd:"" help:"Manage the local Airbyte installation."` + Images images.Cmd `cmd:"" help:"Manage images used by Airbyte and abctl."` Version version.Cmd `cmd:"" help:"Display version information."` Verbose verbose `short:"v" help:"Enable verbose output."` } func (c *Cmd) BeforeApply(ctx *kong.Context) error { - if _, envVarDNT := os.LookupEnv("DO_NOT_TRACK"); envVarDNT { - pterm.Info.Println("Telemetry collection disabled (DO_NOT_TRACK)") - } ctx.BindTo(k8s.DefaultProvider, (*k8s.Provider)(nil)) ctx.BindTo(telemetry.Get(), (*telemetry.Client)(nil)) if err := ctx.BindToProvider(bindK8sClient(&k8s.DefaultProvider)); err != nil { diff --git a/internal/cmd/images/images_cmd.go b/internal/cmd/images/images_cmd.go new file mode 100644 index 0000000..adc46ef --- /dev/null +++ b/internal/cmd/images/images_cmd.go @@ -0,0 +1,5 @@ +package images + +type Cmd struct { + Manifest ManifestCmd `cmd:"" help:"Display a manifest of images used by Airbyte and abctl."` +} diff --git a/internal/cmd/images/manifest_cmd.go b/internal/cmd/images/manifest_cmd.go new file mode 100644 index 0000000..e257b68 --- /dev/null +++ b/internal/cmd/images/manifest_cmd.go @@ -0,0 +1,166 @@ +package images + +import ( + "fmt" + "slices" + "strings" + + "github.com/airbytehq/abctl/internal/cmd/local/helm" + "github.com/airbytehq/abctl/internal/cmd/local/k8s" + helmlib "github.com/mittwald/go-helm-client" + "helm.sh/helm/v3/pkg/repo" + + "github.com/airbytehq/abctl/internal/common" + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/kubectl/pkg/scheme" +) + +type ManifestCmd struct { + Chart string `help:"Path to chart." xor:"chartver"` + ChartVersion string `help:"Version of the chart." xor:"chartver"` + Values string `type:"existingfile" help:"An Airbyte helm chart values file to configure helm."` +} + +func (c *ManifestCmd) Run(provider k8s.Provider) error { + + client, err := helm.New(provider.Kubeconfig, provider.Context, common.AirbyteNamespace) + if err != nil { + return err + } + + images, err := c.findAirbyteImages(client) + if err != nil { + return err + } + + for _, img := range images { + fmt.Println(img) + } + + return nil +} + +func (c *ManifestCmd) findAirbyteImages(client helm.Client) ([]string, error) { + valuesYaml, err := helm.BuildAirbyteValues(helm.ValuesOpts{ + ValuesFile: c.Values, + }) + if err != nil { + return nil, err + } + + airbyteChartLoc := helm.LocateLatestAirbyteChart(c.ChartVersion, c.Chart) + return findImagesFromChart(client, valuesYaml, airbyteChartLoc, c.ChartVersion) +} + +func findImagesFromChart(client helm.Client, valuesYaml, chartName, chartVersion string) ([]string, error) { + err := client.AddOrUpdateChartRepo(repo.Entry{ + Name: common.AirbyteRepoName, + URL: common.AirbyteRepoURL, + }) + if err != nil { + return nil, err + } + + bytes, err := client.TemplateChart(&helmlib.ChartSpec{ + ChartName: chartName, + GenerateName: true, + ValuesYaml: valuesYaml, + Version: chartVersion, + }, nil) + if err != nil { + return nil, err + } + + images := findAllImages(string(bytes)) + return images, nil +} + +// findAllImages walks through the Helm chart, looking for container images in k8s PodSpecs. +// It also looks for env vars in the airbyte-env config map that end with "_IMAGE". +// It returns a unique, sorted list of images found. +func findAllImages(chartYaml string) []string { + objs := decodeK8sResources(chartYaml) + imageSet := set[string]{} + + for _, obj := range objs { + + var podSpec *corev1.PodSpec + switch z := obj.(type) { + case *corev1.ConfigMap: + if strings.HasSuffix(z.Name, "airbyte-env") { + for k, v := range z.Data { + if strings.HasSuffix(k, "_IMAGE") { + imageSet.add(v) + } + } + } + continue + case *corev1.Pod: + podSpec = &z.Spec + case *batchv1.Job: + podSpec = &z.Spec.Template.Spec + case *appsv1.Deployment: + podSpec = &z.Spec.Template.Spec + case *appsv1.StatefulSet: + podSpec = &z.Spec.Template.Spec + default: + continue + } + + for _, c := range podSpec.InitContainers { + imageSet.add(c.Image) + } + for _, c := range podSpec.Containers { + imageSet.add(c.Image) + } + } + + var out []string + for _, k := range imageSet.items() { + if k != "" { + out = append(out, k) + } + } + slices.Sort(out) + + return out +} + +func decodeK8sResources(renderedYaml string) []runtime.Object { + out := []runtime.Object{} + chunks := strings.Split(renderedYaml, "---") + for _, chunk := range chunks { + if len(chunk) == 0 { + continue + } + obj, _, err := scheme.Codecs.UniversalDeserializer().Decode([]byte(chunk), nil, nil) + if err != nil { + continue + } + out = append(out, obj) + } + return out +} + +type set[T comparable] struct { + vals map[T]struct{} +} + +func (s *set[T]) add(v T) { + if s.vals == nil { + s.vals = map[T]struct{}{} + } + s.vals[v] = struct{}{} +} + +func (s *set[T]) items() []T { + out := make([]T, len(s.vals)) + for k := range s.vals { + out = append(out, k) + } + return out +} \ No newline at end of file diff --git a/internal/cmd/images/manifest_cmd_test.go b/internal/cmd/images/manifest_cmd_test.go new file mode 100644 index 0000000..708e689 --- /dev/null +++ b/internal/cmd/images/manifest_cmd_test.go @@ -0,0 +1,127 @@ +package images + +import ( + "sort" + "testing" + + "github.com/google/go-cmp/cmp" + helmlib "github.com/mittwald/go-helm-client" + + "github.com/airbytehq/abctl/internal/cmd/local/helm" +) + +func getHelmTestClient(t *testing.T) helm.Client { + client, err := helmlib.New(nil) + if err != nil { + t.Fatal(err) + } + return client +} + +func TestManifestCmd(t *testing.T) { + client := getHelmTestClient(t) + cmd := ManifestCmd{ + ChartVersion: "1.1.0", + } + actual, err := cmd.findAirbyteImages(client) + if err != nil { + t.Fatal(err) + } + expect := []string{ + "airbyte/bootloader:1.1.0", + "airbyte/connector-builder-server:1.1.0", + "airbyte/cron:1.1.0", + "airbyte/db:1.1.0", + "airbyte/mc", + "airbyte/server:1.1.0", + "airbyte/webapp:1.1.0", + "airbyte/worker:1.1.0", + "airbyte/workload-api-server:1.1.0", + "airbyte/workload-launcher:1.1.0", + "bitnami/kubectl:1.28.9", + "busybox", + "minio/minio:RELEASE.2023-11-20T22-40-07Z", + "temporalio/auto-setup:1.23.0", + } + compareList(t, expect, actual) +} + +func TestManifestCmd_Enterprise(t *testing.T) { + client := getHelmTestClient(t) + cmd := ManifestCmd{ + ChartVersion: "1.1.0", + Values: "testdata/enterprise.values.yaml", + } + actual, err := cmd.findAirbyteImages(client) + if err != nil { + t.Fatal(err) + } + expect := []string{ + "airbyte/bootloader:1.1.0", + "airbyte/connector-builder-server:1.1.0", + "airbyte/cron:1.1.0", + "airbyte/db:1.1.0", + "airbyte/keycloak-setup:1.1.0", + "airbyte/keycloak:1.1.0", + "airbyte/mc", + "airbyte/server:1.1.0", + "airbyte/webapp:1.1.0", + "airbyte/worker:1.1.0", + "airbyte/workload-api-server:1.1.0", + "airbyte/workload-launcher:1.1.0", + "bitnami/kubectl:1.28.9", + "busybox", + "curlimages/curl:8.1.1", + "minio/minio:RELEASE.2023-11-20T22-40-07Z", + "postgres:13-alpine", + "temporalio/auto-setup:1.23.0", + } + compareList(t, expect, actual) +} + +func TestManifestCmd_Nightly(t *testing.T) { + client := getHelmTestClient(t) + cmd := ManifestCmd{ + // This version includes chart fixes that expose images more consistently and completely. + ChartVersion: "1.1.0-nightly-1728428783-9025e1a46e", + Values: "testdata/enterprise.values.yaml", + } + actual, err := cmd.findAirbyteImages(client) + if err != nil { + t.Fatal(err) + } + expect := []string{ + "airbyte/bootloader:nightly-1728428783-9025e1a46e", + "airbyte/connector-builder-server:nightly-1728428783-9025e1a46e", + "airbyte/connector-sidecar:nightly-1728428783-9025e1a46e", + "airbyte/container-orchestrator:nightly-1728428783-9025e1a46e", + "airbyte/cron:nightly-1728428783-9025e1a46e", + "airbyte/db:nightly-1728428783-9025e1a46e", + "airbyte/keycloak-setup:nightly-1728428783-9025e1a46e", + "airbyte/keycloak:nightly-1728428783-9025e1a46e", + "airbyte/mc:latest", + "airbyte/server:nightly-1728428783-9025e1a46e", + "airbyte/webapp:nightly-1728428783-9025e1a46e", + "airbyte/worker:nightly-1728428783-9025e1a46e", + "airbyte/workload-api-server:nightly-1728428783-9025e1a46e", + "airbyte/workload-init-container:nightly-1728428783-9025e1a46e", + "airbyte/workload-launcher:nightly-1728428783-9025e1a46e", + "bitnami/kubectl:1.28.9", + "busybox:1.35", + "busybox:latest", + "curlimages/curl:8.1.1", + "minio/minio:RELEASE.2023-11-20T22-40-07Z", + "postgres:13-alpine", + "temporalio/auto-setup:1.23.0", + } + compareList(t, expect, actual) +} + +func compareList(t *testing.T, expect, actual []string) { + t.Helper() + sort.Strings(expect) + sort.Strings(actual) + if d := cmp.Diff(expect, actual); d != "" { + t.Error(d) + } +} diff --git a/internal/cmd/images/testdata/enterprise.values.yaml b/internal/cmd/images/testdata/enterprise.values.yaml new file mode 100644 index 0000000..76f498e --- /dev/null +++ b/internal/cmd/images/testdata/enterprise.values.yaml @@ -0,0 +1,14 @@ +global: + airbyteUrl: "http://localhost:8000" + edition: "enterprise" + + auth: + enabled: false + instanceAdmin: + firstName: "test" + lastName: "user" + +keycloak: + auth: + adminUsername: airbyteAdmin + adminPassword: keycloak123 \ No newline at end of file diff --git a/internal/cmd/local/check_test.go b/internal/cmd/local/check_test.go index ffb9a02..1d35a0d 100644 --- a/internal/cmd/local/check_test.go +++ b/internal/cmd/local/check_test.go @@ -17,7 +17,6 @@ import ( "github.com/docker/docker/api/types/system" "github.com/docker/go-connections/nat" "github.com/google/go-cmp/cmp" - "github.com/google/uuid" ) func TestDockerInstalled(t *testing.T) { @@ -40,9 +39,7 @@ func TestDockerInstalled(t *testing.T) { }, } - tel := mockTelemetryClient{ - attr: func(key, val string) {}, - } + tel := telemetry.MockClient{} version, err := dockerInstalled(context.Background(), &tel) if err != nil { @@ -73,7 +70,7 @@ func TestDockerInstalled_Error(t *testing.T) { }, } - _, err := dockerInstalled(context.Background(), &mockTelemetryClient{}) + _, err := dockerInstalled(context.Background(), &telemetry.MockClient{}) if err == nil { t.Error("unexpected error:", err) } @@ -303,39 +300,3 @@ func port(s string) int { p, _ := strconv.Atoi(vals[len(vals)-1]) return p } - -// --- mocks -var _ telemetry.Client = (*mockTelemetryClient)(nil) - -type mockTelemetryClient struct { - start func(ctx context.Context, eventType telemetry.EventType) error - success func(ctx context.Context, eventType telemetry.EventType) error - failure func(ctx context.Context, eventType telemetry.EventType, err error) error - attr func(key, val string) - user func() uuid.UUID - wrap func(context.Context, telemetry.EventType, func() error) error -} - -func (m *mockTelemetryClient) Start(ctx context.Context, eventType telemetry.EventType) error { - return m.start(ctx, eventType) -} - -func (m *mockTelemetryClient) Success(ctx context.Context, eventType telemetry.EventType) error { - return m.success(ctx, eventType) -} - -func (m *mockTelemetryClient) Failure(ctx context.Context, eventType telemetry.EventType, err error) error { - return m.failure(ctx, eventType, err) -} - -func (m *mockTelemetryClient) Attr(key, val string) { - m.attr(key, val) -} - -func (m *mockTelemetryClient) User() uuid.UUID { - return m.user() -} - -func (m *mockTelemetryClient) Wrap(ctx context.Context, et telemetry.EventType, f func() error) error { - return m.wrap(ctx, et, f) -} diff --git a/internal/cmd/local/helm/airbyte_values.go b/internal/cmd/local/helm/airbyte_values.go new file mode 100644 index 0000000..cf3d9c1 --- /dev/null +++ b/internal/cmd/local/helm/airbyte_values.go @@ -0,0 +1,76 @@ +package helm + +import ( + "fmt" + + "github.com/airbytehq/abctl/internal/maps" +) + +type ValuesOpts struct { + ValuesFile string + LowResourceMode bool + InsecureCookies bool + TelemetryUser string + ImagePullSecret string +} + +func BuildAirbyteValues(opts ValuesOpts) (string, error) { + + vals := []string{ + "global.env_vars.AIRBYTE_INSTALLATION_ID=" + opts.TelemetryUser, + "global.auth.enabled=true", + "global.jobs.resources.limits.cpu=3", + "global.jobs.resources.limits.memory=4Gi", + } + + if opts.LowResourceMode { + vals = append(vals, + "server.env_vars.JOB_RESOURCE_VARIANT_OVERRIDE=lowresource", + "global.jobs.resources.requests.cpu=0", + "global.jobs.resources.requests.memory=0", + + "workload-launcher.env_vars.CHECK_JOB_MAIN_CONTAINER_CPU_REQUEST=0", + "workload-launcher.env_vars.CHECK_JOB_MAIN_CONTAINER_MEMORY_REQUEST=0", + "workload-launcher.env_vars.DISCOVER_JOB_MAIN_CONTAINER_CPU_REQUEST=0", + "workload-launcher.env_vars.DISCOVER_JOB_MAIN_CONTAINER_MEMORY_REQUEST=0", + "workload-launcher.env_vars.SPEC_JOB_MAIN_CONTAINER_CPU_REQUEST=0", + "workload-launcher.env_vars.SPEC_JOB_MAIN_CONTAINER_MEMORY_REQUEST=0", + "workload-launcher.env_vars.SIDECAR_MAIN_CONTAINER_CPU_REQUEST=0", + "workload-launcher.env_vars.SIDECAR_MAIN_CONTAINER_MEMORY_REQUEST=0", + ) + } + + if opts.ImagePullSecret != "" { + vals = append(vals, fmt.Sprintf("global.imagePullSecrets[0].name=%s", opts.ImagePullSecret)) + } + + if opts.InsecureCookies { + vals = append(vals, "global.auth.cookieSecureSetting=false") + } + + fileVals, err := maps.FromYAMLFile(opts.ValuesFile) + if err != nil { + return "", err + } + + return mergeValuesWithValuesYAML(vals, fileVals) +} + +// mergeValuesWithValuesYAML ensures that the values defined within this code have a lower +// priority than any values defined in a values.yaml file. +// By default, the helm-client we're using reversed this priority, putting the values +// defined in this code at a higher priority than the values defined in the values.yaml file. +// This function returns a string representation of the value.yaml file after all +// values provided were potentially overridden by the valuesYML file. +func mergeValuesWithValuesYAML(values []string, userValues map[string]any) (string, error) { + a := maps.FromSlice(values) + + maps.Merge(a, userValues) + + res, err := maps.ToYAML(a) + if err != nil { + return "", fmt.Errorf("unable to merge values: %w", err) + } + + return res, nil +} diff --git a/internal/cmd/local/helm/helm.go b/internal/cmd/local/helm/helm.go index dc1eeb7..fab4ff3 100644 --- a/internal/cmd/local/helm/helm.go +++ b/internal/cmd/local/helm/helm.go @@ -23,6 +23,7 @@ type Client interface { GetRelease(name string) (*release.Release, error) InstallOrUpgradeChart(ctx context.Context, spec *helmclient.ChartSpec, opts *helmclient.GenericHelmOptions) (*release.Release, error) UninstallReleaseByName(name string) error + TemplateChart(spec *helmclient.ChartSpec, options *helmclient.HelmTemplateOptions) ([]byte, error) } // New returns the default helm client diff --git a/internal/cmd/local/local/locate.go b/internal/cmd/local/helm/locate.go similarity index 89% rename from internal/cmd/local/local/locate.go rename to internal/cmd/local/helm/locate.go index bca2b39..8a31bd8 100644 --- a/internal/cmd/local/local/locate.go +++ b/internal/cmd/local/helm/locate.go @@ -1,10 +1,11 @@ -package local +package helm import ( "errors" "fmt" "strings" + "github.com/airbytehq/abctl/internal/common" "github.com/pterm/pterm" "golang.org/x/mod/semver" "helm.sh/helm/v3/pkg/cli" @@ -40,8 +41,8 @@ var defaultNewChartRepo newChartRepo = func(cfg *repo.Entry, getters getter.Prov // This variable should only be modified for testing purposes. var defaultLoadIndexFile loadIndexFile = repo.LoadIndexFile -func locateLatestAirbyteChart(chartName, chartVersion, chartFlag string) string { - pterm.Debug.Printf("getting helm chart %q with version %q\n", chartName, chartVersion) +func LocateLatestAirbyteChart(chartVersion, chartFlag string) string { + pterm.Debug.Printf("getting helm chart %q with version %q\n", common.AirbyteChartName, chartVersion) // If the --chart flag was given, use that. if chartFlag != "" { @@ -55,8 +56,8 @@ func locateLatestAirbyteChart(chartName, chartVersion, chartFlag string) string // Here we avoid that problem by figuring out the full URL of the airbyte chart, // which forces Helm to resolve the chart over HTTP and ignore local directories. // If the locator fails, fall back to the original helm behavior. - if chartName == airbyteChartName && chartVersion == "" { - if url, err := getLatestAirbyteChartUrlFromRepoIndex(airbyteRepoName, airbyteRepoURL); err == nil { + if chartVersion == "" { + if url, err := getLatestAirbyteChartUrlFromRepoIndex(common.AirbyteRepoName, common.AirbyteRepoURL); err == nil { pterm.Debug.Printf("determined latest airbyte chart url: %s\n", url) return url } else { @@ -64,7 +65,7 @@ func locateLatestAirbyteChart(chartName, chartVersion, chartFlag string) string } } - return chartName + return common.AirbyteChartName } func getLatestAirbyteChartUrlFromRepoIndex(repoName, repoUrl string) (string, error) { @@ -117,5 +118,5 @@ func getLatestAirbyteChartUrlFromRepoIndex(repoName, repoUrl string) (string, er return "", fmt.Errorf("unexpected number of URLs - %d", len(latest.URLs)) } - return airbyteRepoURL + "/" + latest.URLs[0], nil + return common.AirbyteRepoURL + "/" + latest.URLs[0], nil } diff --git a/internal/cmd/local/local/locate_test.go b/internal/cmd/local/helm/locate_test.go similarity index 88% rename from internal/cmd/local/local/locate_test.go rename to internal/cmd/local/helm/locate_test.go index 9977483..81befc1 100644 --- a/internal/cmd/local/local/locate_test.go +++ b/internal/cmd/local/helm/locate_test.go @@ -1,8 +1,9 @@ -package local +package helm import ( "testing" + "github.com/airbytehq/abctl/internal/common" "github.com/google/go-cmp/cmp" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/getter" @@ -11,7 +12,7 @@ import ( func TestLocateChartFlag(t *testing.T) { expect := "chartFlagValue" - c := locateLatestAirbyteChart("airbyte", "", expect) + c := LocateLatestAirbyteChart("", expect) if c != expect { t.Errorf("expected %q but got %q", expect, c) } @@ -40,7 +41,7 @@ func TestLocate(t *testing.T) { URLs: []string{"example.test"}, }}, }, - exp: airbyteRepoURL + "/example.test", + exp: common.AirbyteRepoURL + "/example.test", }, { name: "one non-release entry", @@ -50,12 +51,12 @@ func TestLocate(t *testing.T) { URLs: []string{"example.test"}, }}, }, - exp: airbyteChartName, + exp: common.AirbyteChartName, }, { name: "no entries", entries: map[string]repo.ChartVersions{}, - exp: airbyteChartName, + exp: common.AirbyteChartName, }, { name: "one release entry with no URLs", @@ -65,7 +66,7 @@ func TestLocate(t *testing.T) { URLs: []string{}, }}, }, - exp: airbyteChartName, + exp: common.AirbyteChartName, }, { name: "one release entry with two URLs", @@ -75,7 +76,7 @@ func TestLocate(t *testing.T) { URLs: []string{"one.test", "two.test"}, }}, }, - exp: airbyteChartName, + exp: common.AirbyteChartName, }, { name: "one non-release entry followed by one release entry", @@ -91,14 +92,14 @@ func TestLocate(t *testing.T) { }, }, }, - exp: airbyteRepoURL + "/good.test", + exp: common.AirbyteRepoURL + "/good.test", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { defaultLoadIndexFile = mockLoadIndexFile(repo.IndexFile{Entries: tt.entries}) - act := locateLatestAirbyteChart(airbyteChartName, "", "") + act := LocateLatestAirbyteChart("", "") if d := cmp.Diff(tt.exp, act); d != "" { t.Errorf("mismatch (-want +got):\n%s", d) } diff --git a/internal/cmd/local/local/nginx.go b/internal/cmd/local/helm/nginx_values.go similarity index 89% rename from internal/cmd/local/local/nginx.go rename to internal/cmd/local/helm/nginx_values.go index 7a65a65..599a343 100644 --- a/internal/cmd/local/local/nginx.go +++ b/internal/cmd/local/helm/nginx_values.go @@ -1,4 +1,4 @@ -package local +package helm import ( "bytes" @@ -22,7 +22,7 @@ controller: proxy-send-timeout: "600" `)) -func getNginxValuesYaml(port int) (string, error) { +func BuildNginxValues(port int) (string, error) { var buf bytes.Buffer err := nginxValuesTpl.Execute(&buf, map[string]any{"Port": port}) if err != nil { diff --git a/internal/cmd/local/local.go b/internal/cmd/local/local.go index e6b4b21..d715e6d 100644 --- a/internal/cmd/local/local.go +++ b/internal/cmd/local/local.go @@ -21,6 +21,11 @@ type Cmd struct { } func (c *Cmd) BeforeApply() error { + + if _, envVarDNT := os.LookupEnv("DO_NOT_TRACK"); envVarDNT { + pterm.Info.Println("Telemetry collection disabled (DO_NOT_TRACK)") + } + if err := checkAirbyteDir(); err != nil { return fmt.Errorf("%w: %w", localerr.ErrAirbyteDir, err) } diff --git a/internal/cmd/local/local/cmd.go b/internal/cmd/local/local/cmd.go index c76291c..f6c650f 100644 --- a/internal/cmd/local/local/cmd.go +++ b/internal/cmd/local/local/cmd.go @@ -5,9 +5,11 @@ import ( "net/http" "time" + "github.com/airbytehq/abctl/internal/cmd/local/docker" "github.com/airbytehq/abctl/internal/cmd/local/helm" "github.com/airbytehq/abctl/internal/cmd/local/k8s/kind" "github.com/airbytehq/abctl/internal/cmd/local/paths" + "github.com/airbytehq/abctl/internal/common" "k8s.io/client-go/rest" "github.com/airbytehq/abctl/internal/cmd/local/k8s" @@ -19,24 +21,6 @@ import ( "k8s.io/client-go/tools/clientcmd" ) -const ( - airbyteBootloaderPodName = "airbyte-abctl-airbyte-bootloader" - airbyteChartName = "airbyte/airbyte" - airbyteChartRelease = "airbyte-abctl" - airbyteIngress = "ingress-abctl" - airbyteNamespace = "airbyte-abctl" - airbyteRepoName = "airbyte" - airbyteRepoURL = "https://airbytehq.github.io/helm-charts" - nginxChartName = "nginx/ingress-nginx" - nginxChartRelease = "ingress-nginx" - nginxNamespace = "ingress-nginx" - nginxRepoName = "nginx" - nginxRepoURL = "https://kubernetes.github.io/ingress-nginx" -) - -// dockerAuthSecretName is the name of the secret which holds the docker authentication information. -const dockerAuthSecretName = "docker-auth" - type HTTPClient interface { Do(req *http.Request) (*http.Response, error) } @@ -44,26 +28,30 @@ type HTTPClient interface { // BrowserLauncher primarily for testing purposes. type BrowserLauncher func(url string) error -// ChartLocator primarily for testing purposes. -type ChartLocator func(repoName, repoUrl, chartFlag string) string - // Command is the local command, responsible for installing, uninstalling, or other local actions. type Command struct { - provider k8s.Provider - http HTTPClient - helm helm.Client - k8s k8s.Client - portHTTP int - spinner *pterm.SpinnerPrinter - tel telemetry.Client - launcher BrowserLauncher - locateChart ChartLocator - userHome string + provider k8s.Provider + docker *docker.Docker + + http HTTPClient + helm helm.Client + k8s k8s.Client + portHTTP int + spinner *pterm.SpinnerPrinter + tel telemetry.Client + launcher BrowserLauncher + userHome string } // Option for configuring the Command, primarily exists for testing type Option func(*Command) +func WithDockerClient(client *docker.Docker) Option { + return func(c *Command) { + c.docker = client + } +} + // WithTelemetryClient define the telemetry client for this command. func WithTelemetryClient(client telemetry.Client) Option { return func(c *Command) { @@ -99,12 +87,6 @@ func WithBrowserLauncher(launcher BrowserLauncher) Option { } } -func WithChartLocator(locator ChartLocator) Option { - return func(c *Command) { - c.locateChart = locator - } -} - // WithUserHome define the user's home directory. func WithUserHome(home string) Option { return func(c *Command) { @@ -131,10 +113,6 @@ func New(provider k8s.Provider, opts ...Option) (*Command, error) { opt(c) } - if c.locateChart == nil { - c.locateChart = locateLatestAirbyteChart - } - // determine userhome if not defined if c.userHome == "" { c.userHome = paths.UserHome @@ -160,7 +138,7 @@ func New(provider k8s.Provider, opts ...Option) (*Command, error) { // set the helm client, if not defined if c.helm == nil { var err error - if c.helm, err = helm.New(provider.Kubeconfig, provider.Context, airbyteNamespace); err != nil { + if c.helm, err = helm.New(provider.Kubeconfig, provider.Context, common.AirbyteNamespace); err != nil { return nil, err } } diff --git a/internal/cmd/local/local/install.go b/internal/cmd/local/local/install.go index 52c7c6e..58a07e3 100644 --- a/internal/cmd/local/local/install.go +++ b/internal/cmd/local/local/install.go @@ -12,14 +12,13 @@ import ( "github.com/airbytehq/abctl/internal/cmd/local/docker" "github.com/airbytehq/abctl/internal/cmd/local/helm" + "github.com/airbytehq/abctl/internal/cmd/local/k8s" "github.com/airbytehq/abctl/internal/cmd/local/localerr" "github.com/airbytehq/abctl/internal/cmd/local/migrate" "github.com/airbytehq/abctl/internal/cmd/local/paths" - "github.com/airbytehq/abctl/internal/maps" + "github.com/airbytehq/abctl/internal/common" "github.com/airbytehq/abctl/internal/telemetry" - "github.com/google/uuid" helmclient "github.com/mittwald/go-helm-client" - "github.com/mittwald/go-helm-client/values" "github.com/pterm/pterm" "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/chart" @@ -42,26 +41,23 @@ const ( ) type InstallOpts struct { - HelmChartFlag string - HelmChartVersion string - HelmValues map[string]any - Secrets []string - Migrate bool - Hosts []string - - Docker *docker.Docker + HelmChartVersion string + HelmValuesYaml string + AirbyteChartLoc string + Secrets []string + Migrate bool + Hosts []string + ExtraVolumeMounts []k8s.ExtraVolumeMount DockerServer string DockerUser string DockerPass string DockerEmail string - NoBrowser bool - LowResourceMode bool - InsecureCookies bool + NoBrowser bool } -func (i *InstallOpts) dockerAuth() bool { +func (i *InstallOpts) DockerAuth() bool { return i.DockerUser != "" && i.DockerPass != "" } @@ -138,85 +134,49 @@ func (c *Command) persistentVolumeClaim(ctx context.Context, namespace, name, vo } // Install handles the installation of Airbyte -func (c *Command) Install(ctx context.Context, opts InstallOpts) error { +func (c *Command) Install(ctx context.Context, opts *InstallOpts) error { go c.watchEvents(ctx) - if !c.k8s.NamespaceExists(ctx, airbyteNamespace) { - c.spinner.UpdateText(fmt.Sprintf("Creating namespace '%s'", airbyteNamespace)) - if err := c.k8s.NamespaceCreate(ctx, airbyteNamespace); err != nil { - pterm.Error.Println(fmt.Sprintf("Unable to create namespace '%s'", airbyteNamespace)) + if !c.k8s.NamespaceExists(ctx, common.AirbyteNamespace) { + c.spinner.UpdateText(fmt.Sprintf("Creating namespace '%s'", common.AirbyteNamespace)) + if err := c.k8s.NamespaceCreate(ctx, common.AirbyteNamespace); err != nil { + pterm.Error.Println(fmt.Sprintf("Unable to create namespace '%s'", common.AirbyteNamespace)) return fmt.Errorf("unable to create airbyte namespace: %w", err) } - pterm.Info.Println(fmt.Sprintf("Namespace '%s' created", airbyteNamespace)) + pterm.Info.Println(fmt.Sprintf("Namespace '%s' created", common.AirbyteNamespace)) } else { - pterm.Info.Printfln("Namespace '%s' already exists", airbyteNamespace) + pterm.Info.Printfln("Namespace '%s' already exists", common.AirbyteNamespace) } - if err := c.persistentVolume(ctx, airbyteNamespace, pvMinio); err != nil { + if err := c.persistentVolume(ctx, common.AirbyteNamespace, pvMinio); err != nil { return err } - if err := c.persistentVolume(ctx, airbyteNamespace, pvPsql); err != nil { + if err := c.persistentVolume(ctx, common.AirbyteNamespace, pvPsql); err != nil { return err } if opts.Migrate { c.spinner.UpdateText("Migrating airbyte data") - if err := c.tel.Wrap(ctx, telemetry.Migrate, func() error { return migrate.FromDockerVolume(ctx, opts.Docker.Client, "airbyte_db") }); err != nil { + if err := c.tel.Wrap(ctx, telemetry.Migrate, func() error { return migrate.FromDockerVolume(ctx, c.docker.Client, "airbyte_db") }); err != nil { pterm.Error.Println("Failed to migrate data from previous Airbyte installation") return fmt.Errorf("unable to migrate data from previous airbyte installation: %w", err) } } - if err := c.persistentVolumeClaim(ctx, airbyteNamespace, pvcMinio, pvMinio); err != nil { + if err := c.persistentVolumeClaim(ctx, common.AirbyteNamespace, pvcMinio, pvMinio); err != nil { return err } - if err := c.persistentVolumeClaim(ctx, airbyteNamespace, pvcPsql, pvPsql); err != nil { + if err := c.persistentVolumeClaim(ctx, common.AirbyteNamespace, pvcPsql, pvPsql); err != nil { return err } - var telUser string - // only override the empty telUser if the tel.User returns a non-nil (uuid.Nil) value. - if c.tel.User() != uuid.Nil { - telUser = c.tel.User().String() - } - - airbyteValues := []string{ - "global.env_vars.AIRBYTE_INSTALLATION_ID=" + telUser, - "global.auth.enabled=true", - "global.jobs.resources.limits.cpu=3", - "global.jobs.resources.limits.memory=4Gi", - } - - if opts.LowResourceMode { - airbyteValues = append(airbyteValues, - "server.env_vars.JOB_RESOURCE_VARIANT_OVERRIDE=lowresource", - "global.jobs.resources.requests.cpu=0", - "global.jobs.resources.requests.memory=0", - - "workload-launcher.env_vars.CHECK_JOB_MAIN_CONTAINER_CPU_REQUEST=0", - "workload-launcher.env_vars.CHECK_JOB_MAIN_CONTAINER_MEMORY_REQUEST=0", - "workload-launcher.env_vars.DISCOVER_JOB_MAIN_CONTAINER_CPU_REQUEST=0", - "workload-launcher.env_vars.DISCOVER_JOB_MAIN_CONTAINER_MEMORY_REQUEST=0", - "workload-launcher.env_vars.SPEC_JOB_MAIN_CONTAINER_CPU_REQUEST=0", - "workload-launcher.env_vars.SPEC_JOB_MAIN_CONTAINER_MEMORY_REQUEST=0", - "workload-launcher.env_vars.SIDECAR_MAIN_CONTAINER_CPU_REQUEST=0", - "workload-launcher.env_vars.SIDECAR_MAIN_CONTAINER_MEMORY_REQUEST=0", - ) - } - - if opts.InsecureCookies { - airbyteValues = append(airbyteValues, - "global.auth.cookieSecureSetting=false") - } - - if opts.dockerAuth() { - pterm.Debug.Println(fmt.Sprintf("Creating '%s' secret", dockerAuthSecretName)) + if opts.DockerAuth() { + pterm.Debug.Println(fmt.Sprintf("Creating '%s' secret", common.DockerAuthSecretName)) if err := c.handleDockerSecret(ctx, opts.DockerServer, opts.DockerUser, opts.DockerPass, opts.DockerEmail); err != nil { - pterm.Debug.Println(fmt.Sprintf("Unable to create '%s' secret", dockerAuthSecretName)) - return fmt.Errorf("unable to create '%s' secret: %w", dockerAuthSecretName, err) + pterm.Debug.Println(fmt.Sprintf("Unable to create '%s' secret", common.DockerAuthSecretName)) + return fmt.Errorf("unable to create '%s' secret: %w", common.DockerAuthSecretName, err) } - pterm.Debug.Println(fmt.Sprintf("Created '%s' secret", dockerAuthSecretName)) - airbyteValues = append(airbyteValues, fmt.Sprintf("global.imagePullSecrets[0].name=%s", dockerAuthSecretName)) + pterm.Debug.Println(fmt.Sprintf("Created '%s' secret", common.DockerAuthSecretName)) } for _, secretFile := range opts.Secrets { @@ -232,7 +192,7 @@ func (c *Command) Install(ctx context.Context, opts InstallOpts) error { pterm.Error.Println(fmt.Sprintf("Unable to unmarshal secret file '%s': %s", secretFile, err)) return fmt.Errorf("unable to unmarshal secret file '%s': %w", secretFile, err) } - secret.ObjectMeta.Namespace = airbyteNamespace + secret.ObjectMeta.Namespace = common.AirbyteNamespace if err := c.k8s.SecretCreateOrUpdate(ctx, secret); err != nil { pterm.Error.Println(fmt.Sprintf("Unable to create secret from file '%s'", secretFile)) @@ -242,26 +202,21 @@ func (c *Command) Install(ctx context.Context, opts InstallOpts) error { pterm.Success.Println(fmt.Sprintf("Secret from '%s' created or updated", secretFile)) } - valuesYAML, err := mergeValuesWithValuesYAML(airbyteValues, opts.HelmValues) - if err != nil { - return err - } - if err := c.handleChart(ctx, chartRequest{ name: "airbyte", - repoName: airbyteRepoName, - repoURL: airbyteRepoURL, - chartName: airbyteChartName, - chartRelease: airbyteChartRelease, - chartFlag: opts.HelmChartFlag, + repoName: common.AirbyteRepoName, + repoURL: common.AirbyteRepoURL, + chartName: common.AirbyteChartName, + chartRelease: common.AirbyteChartRelease, chartVersion: opts.HelmChartVersion, - namespace: airbyteNamespace, - valuesYAML: valuesYAML, + chartLoc: opts.AirbyteChartLoc, + namespace: common.AirbyteNamespace, + valuesYAML: opts.HelmValuesYaml, }); err != nil { return c.diagnoseAirbyteChartFailure(ctx, err) } - nginxValues, err := getNginxValuesYaml(c.portHTTP) + nginxValues, err := helm.BuildNginxValues(c.portHTTP) if err != nil { return err } @@ -270,11 +225,12 @@ func (c *Command) Install(ctx context.Context, opts InstallOpts) error { if err := c.handleChart(ctx, chartRequest{ name: "nginx", uninstallFirst: true, - repoName: nginxRepoName, - repoURL: nginxRepoURL, - chartName: nginxChartName, - chartRelease: nginxChartRelease, - namespace: nginxNamespace, + repoName: common.NginxRepoName, + repoURL: common.NginxRepoURL, + chartName: common.NginxChartName, + chartLoc: common.NginxChartName, + chartRelease: common.NginxChartRelease, + namespace: common.NginxNamespace, valuesYAML: nginxValues, }); err != nil { // If we timed out, there is a good chance it's due to an unavailable port, check if this is the case. @@ -282,9 +238,9 @@ func (c *Command) Install(ctx context.Context, opts InstallOpts) error { if strings.Contains(err.Error(), "client rate limiter Wait returned an error") { pterm.Warning.Printfln("Encountered an error while installing the %s Helm Chart.\n"+ "This could be an indication that port %d is not available.\n"+ - "If installation fails, please try again with a different port.", nginxChartName, c.portHTTP) + "If installation fails, please try again with a different port.", common.NginxChartName, c.portHTTP) - srv, err := c.k8s.ServiceGet(ctx, nginxNamespace, "ingress-nginx-controller") + srv, err := c.k8s.ServiceGet(ctx, common.NginxNamespace, "ingress-nginx-controller") // If there is an error, we can ignore it as we only are checking for a missing ingress entry, // and an error would indicate the inability to check for that entry. if err == nil { @@ -322,14 +278,14 @@ func (c *Command) Install(ctx context.Context, opts InstallOpts) error { func (c *Command) diagnoseAirbyteChartFailure(ctx context.Context, chartErr error) error { - if podList, err := c.k8s.PodList(ctx, airbyteNamespace); err == nil { + if podList, err := c.k8s.PodList(ctx, common.AirbyteNamespace); err == nil { var errors []string for _, pod := range podList.Items { if pod.Status.Phase == corev1.PodFailed { msg := "unknown" - logs, err := c.k8s.LogsGet(ctx, airbyteNamespace, pod.Name) + logs, err := c.k8s.LogsGet(ctx, common.AirbyteNamespace, pod.Name) if err != nil { msg = "unknown: failed to get pod logs." } @@ -354,9 +310,9 @@ func (c *Command) diagnoseAirbyteChartFailure(ctx context.Context, chartErr erro func (c *Command) handleIngress(ctx context.Context, hosts []string) error { c.spinner.UpdateText("Checking for existing Ingress") - if c.k8s.IngressExists(ctx, airbyteNamespace, airbyteIngress) { + if c.k8s.IngressExists(ctx, common.AirbyteNamespace, common.AirbyteIngress) { pterm.Success.Println("Found existing Ingress") - if err := c.k8s.IngressUpdate(ctx, airbyteNamespace, ingress(hosts)); err != nil { + if err := c.k8s.IngressUpdate(ctx, common.AirbyteNamespace, ingress(hosts)); err != nil { pterm.Error.Printfln("Unable to update existing Ingress") return fmt.Errorf("unable to update existing ingress: %w", err) } @@ -365,7 +321,7 @@ func (c *Command) handleIngress(ctx context.Context, hosts []string) error { } pterm.Info.Println("No existing Ingress found, creating one") - if err := c.k8s.IngressCreate(ctx, airbyteNamespace, ingress(hosts)); err != nil { + if err := c.k8s.IngressCreate(ctx, common.AirbyteNamespace, ingress(hosts)); err != nil { pterm.Error.Println("Unable to create ingress") return fmt.Errorf("unable to create ingress: %w", err) } @@ -374,7 +330,7 @@ func (c *Command) handleIngress(ctx context.Context, hosts []string) error { } func (c *Command) watchEvents(ctx context.Context) { - watcher, err := c.k8s.EventsWatch(ctx, airbyteNamespace) + watcher, err := c.k8s.EventsWatch(ctx, common.AirbyteNamespace) if err != nil { pterm.Warning.Printfln("Unable to watch airbyte events\n %s", err) return @@ -427,7 +383,7 @@ func (c *Command) watchBootloaderLogs(ctx context.Context) { // Wait a few seconds on the first iteration, give the bootloaders some time to start. time.Sleep(5 * time.Second) - err := c.streamPodLogs(ctx, airbyteNamespace, airbyteBootloaderPodName, "airbyte-bootloader", since) + err := c.streamPodLogs(ctx, common.AirbyteNamespace, common.AirbyteBootloaderPodName, "airbyte-bootloader", since) if err == nil { break } else { @@ -510,8 +466,8 @@ func (c *Command) handleDockerSecret(ctx context.Context, server, user, pass, em secret := corev1.Secret{ TypeMeta: metav1.TypeMeta{}, ObjectMeta: metav1.ObjectMeta{ - Namespace: airbyteNamespace, - Name: dockerAuthSecretName, + Namespace: common.AirbyteNamespace, + Name: common.DockerAuthSecretName, }, Data: map[string][]byte{corev1.DockerConfigJsonKey: secretBody}, Type: corev1.SecretTypeDockerConfigJson, @@ -532,10 +488,9 @@ type chartRequest struct { repoURL string chartName string chartRelease string - chartFlag string + chartLoc string chartVersion string namespace string - values []string valuesYAML string uninstallFirst bool } @@ -557,9 +512,9 @@ func (c *Command) handleChart( c.spinner.UpdateText(fmt.Sprintf("Fetching %s Helm Chart with version", req.chartName)) - chartLoc := c.locateChart(req.chartName, req.chartVersion, req.chartFlag) + // chartLoc := c.locateChart(req.chartName, req.chartVersion, req.chartFlag) - helmChart, _, err := c.helm.GetChart(chartLoc, &action.ChartPathOptions{Version: req.chartVersion}) + helmChart, _, err := c.helm.GetChart(req.chartLoc, &action.ChartPathOptions{Version: req.chartVersion}) if err != nil { return fmt.Errorf("unable to fetch helm chart %q: %w", req.chartName, err) } @@ -600,12 +555,11 @@ func (c *Command) handleChart( )) helmRelease, err := c.helm.InstallOrUpgradeChart(ctx, &helmclient.ChartSpec{ ReleaseName: req.chartRelease, - ChartName: chartLoc, + ChartName: req.chartLoc, CreateNamespace: true, Namespace: req.namespace, Wait: true, Timeout: 60 * time.Minute, - ValuesOptions: values.Options{Values: req.values}, ValuesYaml: req.valuesYAML, Version: req.chartVersion, }, @@ -746,22 +700,3 @@ func determineHelmChartAction(helm helm.Client, chart *chart.Chart, releaseName return none } - -// mergeValuesWithValuesYAML ensures that the values defined within this code have a lower -// priority than any values defined in a values.yaml file. -// By default, the helm-client we're using reversed this priority, putting the values -// defined in this code at a higher priority than the values defined in the values.yaml file. -// This function returns a string representation of the value.yaml file after all -// values provided were potentially overridden by the valuesYML file. -func mergeValuesWithValuesYAML(values []string, userValues map[string]any) (string, error) { - a := maps.FromSlice(values) - - maps.Merge(a, userValues) - - res, err := maps.ToYAML(a) - if err != nil { - return "", fmt.Errorf("unable to merge values: %w", err) - } - - return res, nil -} diff --git a/internal/cmd/local/local/install_test.go b/internal/cmd/local/local/install_test.go index 0394f76..26353e8 100644 --- a/internal/cmd/local/local/install_test.go +++ b/internal/cmd/local/local/install_test.go @@ -4,83 +4,65 @@ import ( "context" "errors" "net/http" + "os" "testing" "time" + "github.com/airbytehq/abctl/internal/cmd/local/helm" "github.com/airbytehq/abctl/internal/cmd/local/k8s" "github.com/airbytehq/abctl/internal/cmd/local/k8s/k8stest" + "github.com/airbytehq/abctl/internal/common" + "github.com/airbytehq/abctl/internal/telemetry" "github.com/google/go-cmp/cmp" - "github.com/google/uuid" helmclient "github.com/mittwald/go-helm-client" "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/repo" - corev1 "k8s.io/api/core/v1" - networkingv1 "k8s.io/api/networking/v1" ) const portTest = 9999 const testAirbyteChartLoc = "https://airbytehq.github.io/helm-charts/airbyte-1.2.3.tgz" -func testChartLocator(chartName, chartVersion, chartFlag string) string { - if chartName == airbyteChartName && chartVersion == "" { - return testAirbyteChartLoc - } - return chartName -} - func TestCommand_Install(t *testing.T) { - expNginxValues, _ := getNginxValuesYaml(9999) + valuesYaml := mustReadFile(t, "testdata/test-edition.values.yaml") expChartRepoCnt := 0 expChartRepo := []struct { name string url string }{ - {name: airbyteRepoName, url: airbyteRepoURL}, - {name: nginxRepoName, url: nginxRepoURL}, + {name: common.AirbyteRepoName, url: common.AirbyteRepoURL}, + {name: common.NginxRepoName, url: common.NginxRepoURL}, } - // userID is for telemetry tracking purposes - userID := uuid.New() - expChartCnt := 0 + expNginxValues, _ := helm.BuildNginxValues(9999) expChart := []struct { chart helmclient.ChartSpec release release.Release }{ { chart: helmclient.ChartSpec{ - ReleaseName: airbyteChartRelease, + ReleaseName: common.AirbyteChartRelease, ChartName: testAirbyteChartLoc, - Namespace: airbyteNamespace, + Namespace: common.AirbyteNamespace, CreateNamespace: true, Wait: true, Timeout: 60 * time.Minute, - ValuesYaml: `global: - auth: - enabled: "true" - env_vars: - AIRBYTE_INSTALLATION_ID: ` + userID.String() + ` - jobs: - resources: - limits: - cpu: "3" - memory: 4Gi -`, + ValuesYaml: valuesYaml, }, release: release.Release{ Chart: &chart.Chart{Metadata: &chart.Metadata{Version: "1.2.3.4"}}, - Name: airbyteChartRelease, - Namespace: airbyteNamespace, + Name: common.AirbyteChartRelease, + Namespace: common.AirbyteNamespace, Version: 0, }, }, { chart: helmclient.ChartSpec{ - ReleaseName: nginxChartRelease, - ChartName: nginxChartName, - Namespace: nginxNamespace, + ReleaseName: common.NginxChartRelease, + ChartName: common.NginxChartName, + Namespace: common.NginxNamespace, CreateNamespace: true, Wait: true, Timeout: 60 * time.Minute, @@ -88,12 +70,13 @@ func TestCommand_Install(t *testing.T) { }, release: release.Release{ Chart: &chart.Chart{Metadata: &chart.Metadata{Version: "4.3.2.1"}}, - Name: nginxChartRelease, - Namespace: nginxNamespace, + Name: common.NginxChartRelease, + Namespace: common.NginxNamespace, Version: 0, }, }, } + helm := mockHelmClient{ addOrUpdateChartRepo: func(entry repo.Entry) error { if d := cmp.Diff(expChartRepo[expChartRepoCnt].name, entry.Name); d != "" { @@ -112,7 +95,7 @@ func TestCommand_Install(t *testing.T) { switch { case name == testAirbyteChartLoc: return &chart.Chart{Metadata: &chart.Metadata{Version: "test.airbyte.version"}}, "", nil - case name == nginxChartName: + case name == common.NginxChartName: return &chart.Chart{Metadata: &chart.Metadata{Version: "test.nginx.version"}}, "", nil default: t.Error("unsupported chart name", name) @@ -122,10 +105,10 @@ func TestCommand_Install(t *testing.T) { getRelease: func(name string) (*release.Release, error) { switch { - case name == airbyteChartRelease: + case name == common.AirbyteChartRelease: t.Error("should not have been called", name) return nil, errors.New("should not have been called") - case name == nginxChartRelease: + case name == common.NginxChartRelease: return nil, errors.New("not found") default: t.Error("unsupported chart name", name) @@ -153,25 +136,12 @@ func TestCommand_Install(t *testing.T) { } k8sClient := k8stest.MockClient{ - FnServerVersionGet: func() (string, error) { - return "test", nil - }, - FnSecretCreateOrUpdate: func(ctx context.Context, secret corev1.Secret) error { - return nil - }, FnIngressExists: func(ctx context.Context, namespace string, ingress string) bool { return false }, - FnIngressCreate: func(ctx context.Context, namespace string, ingress *networkingv1.Ingress) error { - return nil - }, } - attrs := map[string]string{} - tel := mockTelemetryClient{ - attr: func(key, val string) { attrs[key] = val }, - user: func() uuid.UUID { return userID }, - } + tel := telemetry.MockClient{} httpClient := mockHTTP{do: func(req *http.Request) (*http.Response, error) { return &http.Response{StatusCode: 200}, nil @@ -187,189 +157,25 @@ func TestCommand_Install(t *testing.T) { WithBrowserLauncher(func(url string) error { return nil }), - WithChartLocator(testChartLocator), ) if err != nil { t.Fatal(err) } - if err := c.Install(context.Background(), InstallOpts{}); err != nil { + installOpts := &InstallOpts{ + HelmValuesYaml: valuesYaml, + AirbyteChartLoc: testAirbyteChartLoc, + } + if err := c.Install(context.Background(), installOpts); err != nil { t.Fatal(err) } } -func TestCommand_Install_HelmValues(t *testing.T) { - expChartRepoCnt := 0 - expChartRepo := []struct { - name string - url string - }{ - {name: airbyteRepoName, url: airbyteRepoURL}, - {name: nginxRepoName, url: nginxRepoURL}, - } - - // userID is for telemetry tracking purposes - userID := uuid.New() - - expChartCnt := 0 - expNginxValues, _ := getNginxValuesYaml(9999) - expChart := []struct { - chart helmclient.ChartSpec - release release.Release - }{ - { - chart: helmclient.ChartSpec{ - ReleaseName: airbyteChartRelease, - ChartName: testAirbyteChartLoc, - Namespace: airbyteNamespace, - CreateNamespace: true, - Wait: true, - Timeout: 60 * time.Minute, - ValuesYaml: `global: - auth: - enabled: "true" - edition: test - env_vars: - AIRBYTE_INSTALLATION_ID: ` + userID.String() + ` - jobs: - resources: - limits: - cpu: "3" - memory: 4Gi -`, - }, - release: release.Release{ - Chart: &chart.Chart{Metadata: &chart.Metadata{Version: "1.2.3.4"}}, - Name: airbyteChartRelease, - Namespace: airbyteNamespace, - Version: 0, - }, - }, - { - chart: helmclient.ChartSpec{ - ReleaseName: nginxChartRelease, - ChartName: nginxChartName, - Namespace: nginxNamespace, - CreateNamespace: true, - Wait: true, - Timeout: 60 * time.Minute, - ValuesYaml: expNginxValues, - }, - release: release.Release{ - Chart: &chart.Chart{Metadata: &chart.Metadata{Version: "4.3.2.1"}}, - Name: nginxChartRelease, - Namespace: nginxNamespace, - Version: 0, - }, - }, - } - helm := mockHelmClient{ - addOrUpdateChartRepo: func(entry repo.Entry) error { - if d := cmp.Diff(expChartRepo[expChartRepoCnt].name, entry.Name); d != "" { - t.Error("chart name mismatch", d) - } - if d := cmp.Diff(expChartRepo[expChartRepoCnt].url, entry.URL); d != "" { - t.Error("chart url mismatch", d) - } - - expChartRepoCnt++ - - return nil - }, - - getChart: func(name string, _ *action.ChartPathOptions) (*chart.Chart, string, error) { - switch { - case name == testAirbyteChartLoc: - return &chart.Chart{Metadata: &chart.Metadata{Version: "test.airbyte.version"}}, "", nil - case name == nginxChartName: - return &chart.Chart{Metadata: &chart.Metadata{Version: "test.nginx.version"}}, "", nil - default: - t.Error("unsupported chart name", name) - return nil, "", errors.New("unexpected chart name") - } - }, - - getRelease: func(name string) (*release.Release, error) { - switch { - case name == airbyteChartRelease: - t.Error("should not have been called", name) - return nil, errors.New("should not have been called") - case name == nginxChartRelease: - return nil, errors.New("not found") - default: - t.Error("unsupported chart name", name) - return nil, errors.New("unexpected chart name") - } - }, - - installOrUpgradeChart: func(ctx context.Context, spec *helmclient.ChartSpec, opts *helmclient.GenericHelmOptions) (*release.Release, error) { - if d := cmp.Diff(&expChart[expChartCnt].chart, spec); d != "" { - t.Error("chart mismatch", d) - } - - defer func() { expChartCnt++ }() - - return &expChart[expChartCnt].release, nil - }, - - uninstallReleaseByName: func(s string) error { - if d := cmp.Diff(expChart[expChartCnt].release.Name, s); d != "" { - t.Error("release mismatch", d) - } - - return nil - }, - } - - k8sClient := k8stest.MockClient{ - FnServerVersionGet: func() (string, error) { - return "test", nil - }, - FnSecretCreateOrUpdate: func(ctx context.Context, secret corev1.Secret) error { - return nil - }, - FnIngressExists: func(ctx context.Context, namespace string, ingress string) bool { - return false - }, - FnIngressCreate: func(ctx context.Context, namespace string, ingress *networkingv1.Ingress) error { - return nil - }, - } - - attrs := map[string]string{} - tel := mockTelemetryClient{ - attr: func(key, val string) { attrs[key] = val }, - user: func() uuid.UUID { return userID }, - } - - httpClient := mockHTTP{do: func(req *http.Request) (*http.Response, error) { - return &http.Response{StatusCode: 200}, nil - }} - - c, err := New( - k8s.TestProvider, - WithPortHTTP(portTest), - WithHelmClient(&helm), - WithK8sClient(&k8sClient), - WithTelemetryClient(&tel), - WithHTTPClient(&httpClient), - WithBrowserLauncher(func(url string) error { - return nil - }), - WithChartLocator(testChartLocator), - ) - +func mustReadFile(t *testing.T, name string) string { + b, err := os.ReadFile(name) if err != nil { t.Fatal(err) } - - helmValues := map[string]any{ - "global": map[string]any{ - "edition": "test", - }, - } - if err := c.Install(context.Background(), InstallOpts{HelmValues: helmValues}); err != nil { - t.Fatal(err) - } + return string(b) } diff --git a/internal/cmd/local/local/mock_test.go b/internal/cmd/local/local/mock_test.go index 01cf47c..d4618a8 100644 --- a/internal/cmd/local/local/mock_test.go +++ b/internal/cmd/local/local/mock_test.go @@ -5,8 +5,6 @@ import ( "net/http" "github.com/airbytehq/abctl/internal/cmd/local/helm" - "github.com/airbytehq/abctl/internal/telemetry" - "github.com/google/uuid" helmclient "github.com/mittwald/go-helm-client" "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/chart" @@ -47,44 +45,8 @@ func (m *mockHelmClient) UninstallReleaseByName(s string) error { return m.uninstallReleaseByName(s) } -var _ telemetry.Client = (*mockTelemetryClient)(nil) - -type mockTelemetryClient struct { - start func(context.Context, telemetry.EventType) error - success func(context.Context, telemetry.EventType) error - failure func(context.Context, telemetry.EventType, error) error - attr func(key, val string) - user func() uuid.UUID - wrap func(context.Context, telemetry.EventType, func() error) error -} - -func (m *mockTelemetryClient) Start(ctx context.Context, eventType telemetry.EventType) error { - return m.start(ctx, eventType) -} - -func (m *mockTelemetryClient) Success(ctx context.Context, eventType telemetry.EventType) error { - return m.success(ctx, eventType) -} - -func (m *mockTelemetryClient) Failure(ctx context.Context, eventType telemetry.EventType, err error) error { - return m.failure(ctx, eventType, err) -} - -func (m *mockTelemetryClient) Attr(key, val string) { - if m.attr != nil { - m.attr(key, val) - } -} - -func (m *mockTelemetryClient) User() uuid.UUID { - if m.user == nil { - return uuid.Nil - } - return m.user() -} - -func (m *mockTelemetryClient) Wrap(ctx context.Context, et telemetry.EventType, f func() error) error { - return m.wrap(ctx, et, f) +func (m *mockHelmClient) TemplateChart(spec *helmclient.ChartSpec, options *helmclient.HelmTemplateOptions) ([]byte, error) { + return nil, nil } var _ HTTPClient = (*mockHTTP)(nil) diff --git a/internal/cmd/local/local/specs.go b/internal/cmd/local/local/specs.go index 272ac1b..fc4b5fd 100644 --- a/internal/cmd/local/local/specs.go +++ b/internal/cmd/local/local/specs.go @@ -4,6 +4,7 @@ import ( "fmt" "slices" + "github.com/airbytehq/abctl/internal/common" networkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -36,8 +37,8 @@ func ingress(hosts []string) *networkingv1.Ingress { return &networkingv1.Ingress{ TypeMeta: metav1.TypeMeta{}, ObjectMeta: metav1.ObjectMeta{ - Name: airbyteIngress, - Namespace: airbyteNamespace, + Name: common.AirbyteIngress, + Namespace: common.AirbyteNamespace, }, Spec: networkingv1.IngressSpec{ IngressClassName: &ingressClassName, @@ -60,7 +61,7 @@ func ingressRule(host string) networkingv1.IngressRule { PathType: &pathType, Backend: networkingv1.IngressBackend{ Service: &networkingv1.IngressServiceBackend{ - Name: fmt.Sprintf("%s-airbyte-webapp-svc", airbyteChartRelease), + Name: fmt.Sprintf("%s-airbyte-webapp-svc", common.AirbyteChartRelease), Port: networkingv1.ServiceBackendPort{ Name: "http", }, diff --git a/internal/cmd/local/local/status.go b/internal/cmd/local/local/status.go index 7548d5d..7ab5573 100644 --- a/internal/cmd/local/local/status.go +++ b/internal/cmd/local/local/status.go @@ -4,12 +4,13 @@ import ( "context" "fmt" + "github.com/airbytehq/abctl/internal/common" "github.com/pterm/pterm" ) // Status handles the status of local Airbyte. func (c *Command) Status(_ context.Context) error { - charts := []string{airbyteChartRelease, nginxChartRelease} + charts := []string{common.AirbyteChartRelease, common.NginxChartRelease} for _, name := range charts { c.spinner.UpdateText(fmt.Sprintf("Verifying %s Helm Chart installation status", name)) diff --git a/internal/cmd/local/local/testdata/expected-default.values.yaml b/internal/cmd/local/local/testdata/expected-default.values.yaml new file mode 100644 index 0000000..5c28fc8 --- /dev/null +++ b/internal/cmd/local/local/testdata/expected-default.values.yaml @@ -0,0 +1,10 @@ +global: + auth: + enabled: "true" + env_vars: + AIRBYTE_INSTALLATION_ID: test-user + jobs: + resources: + limits: + cpu: "3" + memory: 4Gi diff --git a/internal/cmd/local/local/testdata/invalid.values.yaml b/internal/cmd/local/local/testdata/invalid.values.yaml new file mode 100644 index 0000000..e7b6e89 --- /dev/null +++ b/internal/cmd/local/local/testdata/invalid.values.yaml @@ -0,0 +1,3 @@ +foo: + - bar: baz + - foo \ No newline at end of file diff --git a/internal/cmd/local/local/testdata/test-edition.values.yaml b/internal/cmd/local/local/testdata/test-edition.values.yaml new file mode 100644 index 0000000..29f39fd --- /dev/null +++ b/internal/cmd/local/local/testdata/test-edition.values.yaml @@ -0,0 +1,2 @@ +global: + edition: test \ No newline at end of file diff --git a/internal/cmd/local/local_install.go b/internal/cmd/local/local_install.go index 6b01f1f..deeadb4 100644 --- a/internal/cmd/local/local_install.go +++ b/internal/cmd/local/local_install.go @@ -5,9 +5,10 @@ import ( "fmt" "strings" + "github.com/airbytehq/abctl/internal/cmd/local/helm" "github.com/airbytehq/abctl/internal/cmd/local/k8s" "github.com/airbytehq/abctl/internal/cmd/local/local" - "github.com/airbytehq/abctl/internal/maps" + "github.com/airbytehq/abctl/internal/common" "github.com/airbytehq/abctl/internal/telemetry" "github.com/pterm/pterm" @@ -31,6 +32,56 @@ type InstallCmd struct { Volume []string `help:"Additional volume mounts. Must be in the format :."` } +func (i *InstallCmd) InstallOpts(user string) (*local.InstallOpts, error) { + extraVolumeMounts, err := parseVolumeMounts(i.Volume) + if err != nil { + return nil, err + } + + for _, host := range i.Host { + if err := validateHostFlag(host); err != nil { + return nil, err + } + } + + opts := &local.InstallOpts{ + HelmChartVersion: i.ChartVersion, + AirbyteChartLoc: helm.LocateLatestAirbyteChart(i.ChartVersion, i.Chart), + Secrets: i.Secret, + Migrate: i.Migrate, + Hosts: i.Host, + ExtraVolumeMounts: extraVolumeMounts, + DockerServer: i.DockerServer, + DockerUser: i.DockerUsername, + DockerPass: i.DockerPassword, + DockerEmail: i.DockerEmail, + NoBrowser: i.NoBrowser, + } + + valuesOpts := helm.ValuesOpts{ + ValuesFile: i.Values, + InsecureCookies: i.InsecureCookies, + LowResourceMode: i.LowResourceMode, + } + + if opts.DockerAuth() { + valuesOpts.ImagePullSecret = common.DockerAuthSecretName + } + + // only override the empty telUser if the tel.User returns a non-nil (uuid.Nil) value. + if user != "" { + valuesOpts.TelemetryUser = user + } + + valuesYAML, err := helm.BuildAirbyteValues(valuesOpts) + if err != nil { + return nil, err + } + opts.HelmValuesYaml = valuesYAML + + return opts, nil +} + func (i *InstallCmd) Run(ctx context.Context, provider k8s.Provider, telClient telemetry.Client) error { spinner := &pterm.DefaultSpinner spinner, _ = spinner.Start("Starting installation") @@ -42,22 +93,11 @@ func (i *InstallCmd) Run(ctx context.Context, provider k8s.Provider, telClient t return fmt.Errorf("unable to determine docker installation status: %w", err) } - helmValues, err := maps.FromYAMLFile(i.Values) + opts, err := i.InstallOpts(telClient.User()) if err != nil { return err } - extraVolumeMounts, err := parseVolumeMounts(i.Volume) - if err != nil { - return err - } - - for _, host := range i.Host { - if err := validateHostFlag(host); err != nil { - return err - } - } - return telClient.Wrap(ctx, telemetry.Install, func() error { spinner.UpdateText(fmt.Sprintf("Checking for existing Kubernetes cluster '%s'", provider.ClusterName)) @@ -97,7 +137,7 @@ func (i *InstallCmd) Run(ctx context.Context, provider k8s.Provider, telClient t pterm.Success.Printfln("Port %d appears to be available", i.Port) spinner.UpdateText(fmt.Sprintf("Creating cluster '%s'", provider.ClusterName)) - if err := cluster.Create(i.Port, extraVolumeMounts); err != nil { + if err := cluster.Create(i.Port, opts.ExtraVolumeMounts); err != nil { pterm.Error.Printfln("Cluster '%s' could not be created", provider.ClusterName) return err } @@ -108,31 +148,13 @@ func (i *InstallCmd) Run(ctx context.Context, provider k8s.Provider, telClient t local.WithPortHTTP(i.Port), local.WithTelemetryClient(telClient), local.WithSpinner(spinner), + local.WithDockerClient(dockerClient), ) if err != nil { pterm.Error.Printfln("Failed to initialize 'local' command") return fmt.Errorf("unable to initialize local command: %w", err) } - opts := local.InstallOpts{ - HelmChartFlag: i.Chart, - HelmChartVersion: i.ChartVersion, - HelmValues: helmValues, - Secrets: i.Secret, - Migrate: i.Migrate, - Docker: dockerClient, - Hosts: i.Host, - - DockerServer: i.DockerServer, - DockerUser: i.DockerUsername, - DockerPass: i.DockerPassword, - DockerEmail: i.DockerEmail, - - NoBrowser: i.NoBrowser, - LowResourceMode: i.LowResourceMode, - InsecureCookies: i.InsecureCookies, - } - if err := lc.Install(ctx, opts); err != nil { spinner.Fail("Unable to install Airbyte locally") return err @@ -148,6 +170,10 @@ func (i *InstallCmd) Run(ctx context.Context, provider k8s.Provider, telClient t } func parseVolumeMounts(specs []string) ([]k8s.ExtraVolumeMount, error) { + if len(specs) == 0 { + return nil, nil + } + mounts := make([]k8s.ExtraVolumeMount, len(specs)) for i, spec := range specs { diff --git a/internal/cmd/local/local_test.go b/internal/cmd/local/local_test.go index ce57c6d..4f16c01 100644 --- a/internal/cmd/local/local_test.go +++ b/internal/cmd/local/local_test.go @@ -10,7 +10,9 @@ import ( "testing" "github.com/airbytehq/abctl/internal/cmd/local/k8s" + "github.com/airbytehq/abctl/internal/cmd/local/local" "github.com/airbytehq/abctl/internal/cmd/local/localerr" + "github.com/alecthomas/kong" "github.com/airbytehq/abctl/internal/cmd/local/paths" "github.com/airbytehq/abctl/internal/telemetry" @@ -99,31 +101,27 @@ func TestCheckAirbyteDir(t *testing.T) { } func TestValues_FileDoesntExist(t *testing.T) { - cmd := InstallCmd{Values: "thisfiledoesnotexist"} - err := cmd.Run(context.Background(), k8s.TestProvider, telemetry.NoopClient{}) + + var root InstallCmd + k, _ := kong.New( + &root, + kong.Name("abctl"), + kong.Description("Airbyte's command line tool for managing a local Airbyte installation."), + kong.UsageOnError(), + ) + _, err := k.Parse([]string{"--values", "/testdata/thisfiledoesnotexist"}) if err == nil { t.Fatal("expected error") } - expect := "failed to read file thisfiledoesnotexist: open thisfiledoesnotexist: no such file or directory" + expect := "--values: stat /testdata/thisfiledoesnotexist: no such file or directory" if err.Error() != expect { t.Errorf("expected %q but got %q", expect, err) } } func TestValues_BadYaml(t *testing.T) { - tmpdir := t.TempDir() - p := filepath.Join(tmpdir, "values.yaml") - content := ` -foo: - - bar: baz - - foo -` - - if err := os.WriteFile(p, []byte(content), 0644); err != nil { - t.Fatal(err) - } - cmd := InstallCmd{Values: p} + cmd := InstallCmd{Values: "./local/testdata/invalid.values.yaml"} err := cmd.Run(context.Background(), k8s.TestProvider, telemetry.NoopClient{}) if err == nil { t.Fatal("expected error") @@ -131,7 +129,6 @@ foo: if !strings.HasPrefix(err.Error(), "failed to unmarshal file") { t.Errorf("unexpected error: %v", err) - } } @@ -150,3 +147,22 @@ func TestInvalidHostFlag_IpAddrWithPort(t *testing.T) { t.Errorf("expected ErrInvalidHostFlag but got %v", err) } } + +func TestInstallOpts(t *testing.T) { + b, _ := os.ReadFile("local/testdata/expected-default.values.yaml") + cmd := InstallCmd{ + // Don't let the code dynamically resolve the latest chart version. + Chart: "/test/path/to/chart", + } + expect := &local.InstallOpts{ + HelmValuesYaml: string(b), + AirbyteChartLoc: "/test/path/to/chart", + } + opts, err := cmd.InstallOpts("test-user") + if err != nil { + t.Fatal(err) + } + if d := cmp.Diff(expect, opts); d != "" { + t.Errorf("unexpected error diff (-want +got):\n%s", d) + } +} diff --git a/internal/common/const.go b/internal/common/const.go new file mode 100644 index 0000000..c4f34a3 --- /dev/null +++ b/internal/common/const.go @@ -0,0 +1,19 @@ +package common + +const ( + AirbyteBootloaderPodName = "airbyte-abctl-airbyte-bootloader" + AirbyteChartName = "airbyte/airbyte" + AirbyteChartRelease = "airbyte-abctl" + AirbyteIngress = "ingress-abctl" + AirbyteNamespace = "airbyte-abctl" + AirbyteRepoName = "airbyte" + AirbyteRepoURL = "https://airbytehq.github.io/helm-charts" + NginxChartName = "nginx/ingress-nginx" + NginxChartRelease = "ingress-nginx" + NginxNamespace = "ingress-nginx" + NginxRepoName = "nginx" + NginxRepoURL = "https://kubernetes.github.io/ingress-nginx" + + // DockerAuthSecretName is the name of the secret which holds the docker authentication information. + DockerAuthSecretName = "docker-auth" +) diff --git a/internal/telemetry/client.go b/internal/telemetry/client.go index a80ab07..4d54fee 100644 --- a/internal/telemetry/client.go +++ b/internal/telemetry/client.go @@ -9,7 +9,6 @@ import ( "path/filepath" "sync" - "github.com/google/uuid" "github.com/pterm/pterm" ) @@ -43,7 +42,7 @@ type Client interface { // Attr should be called to add additional attributes to this activity. Attr(key, val string) // User returns the user identifier being used by this client - User() uuid.UUID + User() string // Wrap wraps the func() error with the EventType, // calling the Start, Failure or Success methods correctly based on // the behavior of the func() error diff --git a/internal/telemetry/mock.go b/internal/telemetry/mock.go new file mode 100644 index 0000000..d1327a6 --- /dev/null +++ b/internal/telemetry/mock.go @@ -0,0 +1,40 @@ +package telemetry + +import "context" + +var _ Client = (*MockClient)(nil) + +type MockClient struct { + attrs map[string]string + start func(context.Context, EventType) error + success func(context.Context, EventType) error + failure func(context.Context, EventType, error) error + wrap func(context.Context, EventType, func() error) error +} + +func (m *MockClient) Start(ctx context.Context, eventType EventType) error { + return m.start(ctx, eventType) +} + +func (m *MockClient) Success(ctx context.Context, eventType EventType) error { + return m.success(ctx, eventType) +} + +func (m *MockClient) Failure(ctx context.Context, eventType EventType, err error) error { + return m.failure(ctx, eventType, err) +} + +func (m *MockClient) Attr(key, val string) { + if m.attrs == nil { + m.attrs = map[string]string{} + } + m.attrs[key] = val +} + +func (m *MockClient) User() string { + return "test-user" +} + +func (m *MockClient) Wrap(ctx context.Context, et EventType, f func() error) error { + return m.wrap(ctx, et, f) +} diff --git a/internal/telemetry/noop.go b/internal/telemetry/noop.go index 88cd281..bfa76da 100644 --- a/internal/telemetry/noop.go +++ b/internal/telemetry/noop.go @@ -2,8 +2,6 @@ package telemetry import ( "context" - - "github.com/google/uuid" ) var _ Client = (*NoopClient)(nil) @@ -26,8 +24,8 @@ func (n NoopClient) Failure(context.Context, EventType, error) error { func (n NoopClient) Attr(_, _ string) {} -func (n NoopClient) User() uuid.UUID { - return uuid.Nil +func (n NoopClient) User() string { + return "" } func (n NoopClient) Wrap(ctx context.Context, et EventType, f func() error) error { diff --git a/internal/telemetry/noop_test.go b/internal/telemetry/noop_test.go index b56e814..0c8b6ec 100644 --- a/internal/telemetry/noop_test.go +++ b/internal/telemetry/noop_test.go @@ -7,7 +7,6 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - "github.com/google/uuid" ) func TestNoopClient(t *testing.T) { @@ -25,7 +24,7 @@ func TestNoopClient(t *testing.T) { cli.Attr("k", "v'") - if d := cmp.Diff(uuid.Nil, cli.User()); d != "" { + if d := cmp.Diff("", cli.User()); d != "" { t.Errorf("user should be nil (-want +got): %s", d) } } diff --git a/internal/telemetry/segment.go b/internal/telemetry/segment.go index df6c733..d0c87f2 100644 --- a/internal/telemetry/segment.go +++ b/internal/telemetry/segment.go @@ -82,8 +82,8 @@ func (s *SegmentClient) Attr(key, val string) { s.attrs[key] = val } -func (s *SegmentClient) User() uuid.UUID { - return s.cfg.AnalyticsID.toUUID() +func (s *SegmentClient) User() string { + return s.cfg.AnalyticsID.toUUID().String() } func (s *SegmentClient) Wrap(ctx context.Context, et EventType, f func() error) error {