Skip to content

Commit

Permalink
feat: Add dev server (AKA LaunchDevly) (#364)
Browse files Browse the repository at this point in the history
This adds a new command to the cli, dev-server. When you start it, an HTTP server starts running on port 8765. It provides APIs that mimic the APIs that LaunchDarkly SDKs use. You can add-projects to the dev server and they will be populated with flag values from the source-environment. You can also add-overrides to change the value served by a given flag. There's also a UI that will display which flag values are being served and allow you to set overrides.

These features combine to provide a more portable solution for local & ephemeral development environments of projects using LaunchDarkly than the current state of the art: creating real environments in LaunchDarkly for each of these environments.
  • Loading branch information
mike-zorn authored Aug 14, 2024
1 parent 98fa323 commit 373bb0a
Show file tree
Hide file tree
Showing 754 changed files with 421,906 additions and 4,658 deletions.
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ The version of this CLI that you are using.
For instance, Ubuntu 16.04, Windows 10, or macOS 14.4.

**Additional context**
Add any other context about the problem here.
Add any other context about the problem here.
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/feature_request.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ A clear and concise description of what you want to happen.
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context about the feature request here.
Add any other context about the feature request here.
39 changes: 34 additions & 5 deletions .github/actions/publish/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ inputs:
tag:
description: 'Tag to upload artifacts to.'
required: true

outputs:
hashes:
description: sha256sum hashes of built artifacts
Expand All @@ -29,17 +30,45 @@ runs:
with:
platforms: linux/amd64,linux/arm64/v8,linux/arm/v7,linux/386
- name: Login to Docker
shell: bash
run: echo $DOCKER_TOKEN | docker login --username $DOCKER_USERNAME --password-stdin

- name: Set up goreleaser
# Note: that we're unable to use the normal goreleaser actions and have to use this docker image.
# This is because the dev server piece of the CLI uses SQLite which requires CGO and cross compilation.
# We're using the goreleaser-cross image to facilitate this. See also: https://github.com/goreleaser/goreleaser-cross
shell: bash
run: |
echo $DOCKER_TOKEN | docker login --username $DOCKER_USERNAME --password-stdin
CONTAINER_ID="$(
docker run --detach \
--volume "$PWD:$PWD" \
--entrypoint tail \
ghcr.io/goreleaser/goreleaser-cross:latest \
-f /dev/null
)"
docker exec --tty "$CONTAINER_ID" dpkg --add-architecture i386
docker exec --tty "$CONTAINER_ID" apt-get update
docker exec --tty "$CONTAINER_ID" apt-get install --no-install-recommends -y -q crossbuild-essential-i386
docker exec --tty "$CONTAINER_ID" git config --global --add safe.directory '*'
echo "CONTAINER_ID=$CONTAINER_ID" >> "$GITHUB_ENV"
- name: Run Goreleaser
uses: goreleaser/goreleaser-action@v5
with:
version: latest
args: release ${{ inputs.dry-run == 'true' && '--skip=publish' || '' }} --config .goreleaser.yaml
shell: bash
run: docker exec
--env GITHUB_TOKEN
--env HOMEBREW_DEPLOY_KEY
--workdir "$PWD"
--tty
"$CONTAINER_ID"
goreleaser release ${{ inputs.dry-run == 'true' && '--skip=publish' || '' }} --config .goreleaser.yaml
env:
GITHUB_TOKEN: ${{ inputs.token }}
HOMEBREW_DEPLOY_KEY: ${{ inputs.homebrew-gh-secret }}
- name: Upload assets
uses: actions/upload-artifact@v4
with:
name: ldcli
path: dist/*
- name: Hash build artifacts for provenance
id: hash
shell: bash
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
dist/
*.log
node_modules/
devserver.db
65 changes: 49 additions & 16 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
version: 2

project_name: ldcli

env:
- GO111MODULE=on # Ensure we aren't using anything in GOPATH when building
- CGO_ENABLED=1 # Needed for SQLite support
- DOCKER_CLI_EXPERIMENTAL=enabled # This is on by default in docker versions >= 20.10

builds:
- id: ldcli
binary: ldcli
Expand All @@ -6,43 +15,68 @@ builds:
- linux
- windows
goarch:
- 386
- "386"
- amd64
- arm64
ldflags:
- -s # Omit all symbol information to minimize binary size
- -w # Omit DWARF to minimize binary size
- -X 'main.version={{.Version}}'
ignore:
- goos: darwin
goarch: "386"
env:
- CGO_ENABLED=0
# The below environment variables set up the c compiler toolchain for CGO.
# Templates are used to vary the toolchain based on OS & platform.
- TOOLCHAIN_BASE=
{{- if eq .Os "darwin" -}}
o
{{- if eq .Arch "arm64" -}}a{{- end -}}
64-clang
{{- else -}}
{{- if eq .Os "windows" -}}/llvm-mingw/bin/{{- end -}}
{{- if eq .Arch "386" -}}i686{{- end -}}
{{- if eq .Arch "arm64" -}}aarch64{{- end -}}
{{- if eq .Arch "amd64" -}}x86_64{{- end -}}
-
{{- if eq .Os "windows" -}}w64-mingw32{{- end -}}
{{- if eq .Os "linux" -}}linux-gnu{{- end -}}
{{- end -}}
- CC={{ .Env.TOOLCHAIN_BASE }}{{ if ne .Os "darwin" }}-gcc{{ end }}
- CXX={{ .Env.TOOLCHAIN_BASE }}{{ if eq .Os "darwin" }}++{{ else }}-g++{{ end }}

dockers:
# AMD64
- image_templates:
- "launchdarkly/ldcli:{{ .Version }}-amd64"
- "launchdarkly/ldcli:v{{ .Major }}-amd64"
- "launchdarkly/ldcli:latest-amd64"
- "launchdarkly/ldcli:{{ .Version }}-amd64"
- "launchdarkly/ldcli:v{{ .Major }}-amd64"
- "launchdarkly/ldcli:latest-amd64"
goos: linux
goarch: amd64
dockerfile: Dockerfile.goreleaser
skip_push: false
build_flag_templates:
- "--pull"
- "--platform=linux/amd64"
- "--pull"
- "--platform=linux/amd64"

# ARM64v8
- image_templates:
- "launchdarkly/ldcli:{{ .Version }}-arm64v8"
- "launchdarkly/ldcli:v{{ .Major }}-arm64v8"
- "launchdarkly/ldcli:latest-arm64v8"
- "launchdarkly/ldcli:{{ .Version }}-arm64v8"
- "launchdarkly/ldcli:v{{ .Major }}-arm64v8"
- "launchdarkly/ldcli:latest-arm64v8"
goos: linux
goarch: arm64
dockerfile: Dockerfile.goreleaser
skip_push: false
build_flag_templates:
- "--pull"
- "--platform=linux/arm64/v8"
- "--pull"
- "--platform=linux/arm64/v8"
docker_manifests:
- name_template: "launchdarkly/ldcli:{{ .Version}}"
skip_push: false
image_templates:
- "launchdarkly/ldcli:{{ .Version }}-amd64"
- "launchdarkly/ldcli:{{ .Version }}-arm64v8"
- "launchdarkly/ldcli:{{ .Version }}-amd64"
- "launchdarkly/ldcli:{{ .Version }}-arm64v8"

- name_template: "launchdarkly/ldcli:v{{ .Major }}"
skip_push: false
Expand All @@ -56,8 +90,7 @@ docker_manifests:
- "launchdarkly/ldcli:latest-amd64"
- "launchdarkly/ldcli:latest-arm64v8"
brews:
-
name: ldcli
- name: ldcli
description: "The official command line interface for managing LaunchDarkly feature flags."
homepage: "https://launchdarkly.com"
repository:
Expand Down
7 changes: 6 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ repos:
exclude: '^vendor/|^.circleci/image/tools'
name: run-go-tests
- id: discarded-stacktrace
exclude: '(^vendor/)'
exclude: '(^vendor/|.*gen\.go)'
- id: go-mod-tidy
- id: go-mod-verify
- id: go-mod-vendor-no-change
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: end-of-file-fixer
exclude: '^vendor'
3 changes: 0 additions & 3 deletions Dockerfile.goreleaser
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
FROM alpine:3.19.1

RUN apk update
RUN apk add --no-cache git

COPY ldcli /ldcli

LABEL homepage="https://www.launchdarkly.com"
Expand Down
44 changes: 26 additions & 18 deletions cmd/cliflags/flags.go
Original file line number Diff line number Diff line change
@@ -1,36 +1,44 @@
package cliflags

const (
BaseURIDefault = "https://app.launchdarkly.com"
BaseURIDefault = "https://app.launchdarkly.com"
DevStreamURIDefault = "https://stream.launchdarkly.com"
PortDefault = "8765"

AccessTokenFlag = "access-token"
AnalyticsOptOut = "analytics-opt-out"
BaseURIFlag = "base-uri"
DataFlag = "data"
EmailsFlag = "emails"
EnvironmentFlag = "environment"
FlagFlag = "flag"
OutputFlag = "output"
ProjectFlag = "project"
RoleFlag = "role"
AccessTokenFlag = "access-token"
AnalyticsOptOut = "analytics-opt-out"
BaseURIFlag = "base-uri"
DataFlag = "data"
DevStreamURIFlag = "dev-stream-uri"
EmailsFlag = "emails"
EnvironmentFlag = "environment"
FlagFlag = "flag"
OutputFlag = "output"
PortFlag = "port"
ProjectFlag = "project"
RoleFlag = "role"

AccessTokenFlagDescription = "LaunchDarkly access token with write-level access"
AnalyticsOptOutDescription = "Opt out of analytics tracking"
BaseURIFlagDescription = "LaunchDarkly base URI"
DevStreamURIDescription = "Streaming service endpoint that the dev server uses to obtain authoritative flag data. This may be a LaunchDarkly or Relay Proxy endpoint"
EnvironmentFlagDescription = "Default environment key"
FlagFlagDescription = "Default feature flag key"
OutputFlagDescription = "Command response output format in either JSON or plain text"
PortFlagDescription = "Port for the dev server to run on"
ProjectFlagDescription = "Default project key"
)

func AllFlagsHelp() map[string]string {
return map[string]string{
AccessTokenFlag: AccessTokenFlagDescription,
AnalyticsOptOut: AnalyticsOptOutDescription,
BaseURIFlag: BaseURIFlagDescription,
EnvironmentFlag: EnvironmentFlagDescription,
FlagFlag: FlagFlagDescription,
OutputFlag: OutputFlagDescription,
ProjectFlag: ProjectFlagDescription,
AccessTokenFlag: AccessTokenFlagDescription,
AnalyticsOptOut: AnalyticsOptOutDescription,
BaseURIFlag: BaseURIFlagDescription,
DevStreamURIFlag: DevStreamURIDescription,
EnvironmentFlag: EnvironmentFlagDescription,
FlagFlag: FlagFlagDescription,
OutputFlag: OutputFlagDescription,
PortFlag: PortFlagDescription,
ProjectFlag: ProjectFlagDescription,
}
}
2 changes: 2 additions & 0 deletions cmd/config/testdata/help.golden
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ Supported settings:
- `access-token`: LaunchDarkly access token with write-level access
- `analytics-opt-out`: Opt out of analytics tracking
- `base-uri`: LaunchDarkly base URI
- `dev-stream-uri`: Streaming service endpoint that the dev server uses to obtain authoritative flag data. This may be a LaunchDarkly or Relay Proxy endpoint
- `environment`: Default environment key
- `flag`: Default feature flag key
- `output`: Command response output format in either JSON or plain text
- `port`: Port for the dev server to run on
- `project`: Default project key

Usage:
Expand Down
60 changes: 60 additions & 0 deletions cmd/dev_server/dev_server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package dev_server

import (
"fmt"

"github.com/spf13/cobra"
"github.com/spf13/viper"

"github.com/launchdarkly/ldcli/cmd/cliflags"
resourcecmd "github.com/launchdarkly/ldcli/cmd/resources"
"github.com/launchdarkly/ldcli/internal/dev_server"
"github.com/launchdarkly/ldcli/internal/resources"
)

func NewDevServerCmd(client resources.Client, ldClient dev_server.Client) *cobra.Command {
cmd := &cobra.Command{
Use: "dev-server",
Short: "Development server",
Long: "Start and use a local development server for overriding flag values.",
}

cmd.PersistentFlags().String(
cliflags.DevStreamURIFlag,
cliflags.DevStreamURIDefault,
cliflags.DevStreamURIDescription,
)
_ = viper.BindPFlag(cliflags.DevStreamURIFlag, cmd.PersistentFlags().Lookup(cliflags.DevStreamURIFlag))

cmd.PersistentFlags().String(
cliflags.PortFlag,
cliflags.PortDefault,
cliflags.PortFlagDescription,
)

_ = viper.BindPFlag(cliflags.PortFlag, cmd.PersistentFlags().Lookup(cliflags.PortFlag))

// Add subcommands here
cmd.AddGroup(&cobra.Group{ID: "projects", Title: "Project commands:"})
cmd.AddCommand(NewListProjectsCmd(client))
cmd.AddCommand(NewGetProjectCmd(client))
cmd.AddCommand(NewSyncProjectCmd(client))
cmd.AddCommand(NewRemoveProjectCmd(client))
cmd.AddCommand(NewAddProjectCmd(client))

cmd.AddGroup(&cobra.Group{ID: "overrides", Title: "Override commands:"})
cmd.AddCommand(NewAddOverrideCmd(client))
cmd.AddCommand(NewRemoveOverrideCmd(client))
cmd.AddGroup(&cobra.Group{ID: "server", Title: "Server commands:"})

cmd.AddCommand(NewStartServerCmd(ldClient))
cmd.AddCommand(NewUICmd())

cmd.SetUsageTemplate(resourcecmd.SubcommandUsageTemplate())

return cmd
}

func getDevServerUrl() string {
return fmt.Sprintf("http://localhost:%s", viper.GetString(cliflags.PortFlag))
}
Loading

0 comments on commit 373bb0a

Please sign in to comment.