Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement environment update functionality in launchdevly ui #423

Merged
merged 7 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 66 additions & 17 deletions internal/dev_server/adapters/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import (
"net/url"
"strconv"

ldapi "github.com/launchdarkly/api-client-go/v14"
"github.com/pkg/errors"

ldapi "github.com/launchdarkly/api-client-go/v14"
)

const ctxKeyApi = ctxKey("adapters.api")
Expand All @@ -24,6 +25,7 @@ func GetApi(ctx context.Context) Api {
type Api interface {
GetSdkKey(ctx context.Context, projectKey, environmentKey string) (string, error)
GetAllFlags(ctx context.Context, projectKey string) ([]ldapi.FeatureFlag, error)
GetProjectEnvironments(ctx context.Context, projectKey string) ([]ldapi.Environment, error)
}

type apiClientApi struct {
Expand Down Expand Up @@ -52,13 +54,59 @@ func (a apiClientApi) GetAllFlags(ctx context.Context, projectKey string) ([]lda
return flags, err
}

func (a apiClientApi) GetProjectEnvironments(ctx context.Context, projectKey string) ([]ldapi.Environment, error) {
log.Printf("Fetching all environments for project '%s'", projectKey)
environments, err := a.getEnvironments(ctx, projectKey, nil)
if err != nil {
err = errors.Wrap(err, "unable to get environments from LD API")
}
return environments, err
}

func (a apiClientApi) getFlags(ctx context.Context, projectKey string, href *string) ([]ldapi.FeatureFlag, error) {
var featureFlags *ldapi.FeatureFlags
return getPaginatedItems(ctx, projectKey, href, func(ctx context.Context, projectKey string, limit, offset *int64) (*ldapi.FeatureFlags, error) {
query := a.apiClient.FeatureFlagsApi.GetFeatureFlags(ctx, projectKey)

if limit != nil {
query = query.Limit(*limit)
}

if offset != nil {
query = query.Offset(*offset)
}

flags, _, err := query.
Execute()
return flags, err
})
}

func (a apiClientApi) getEnvironments(ctx context.Context, projectKey string, href *string) ([]ldapi.Environment, error) {
return getPaginatedItems(ctx, projectKey, href, func(ctx context.Context, projectKey string, limit, offset *int64) (*ldapi.Environments, error) {
request := a.apiClient.EnvironmentsApi.GetEnvironmentsByProject(ctx, projectKey)
if limit != nil {
request = request.Limit(*limit)
}

if offset != nil {
request = request.Offset(*offset)
}

envs, _, err := request.
Execute()
return envs, err
})
}

func getPaginatedItems[T any, R interface {
GetItems() []T
GetLinks() map[string]ldapi.Link
}](ctx context.Context, projectKey string, href *string, fetchFunc func(context.Context, string, *int64, *int64) (R, error)) ([]T, error) {
var result R
var err error

if href == nil {
featureFlags, _, err = a.apiClient.FeatureFlagsApi.GetFeatureFlags(ctx, projectKey).
Summary(false).
Execute()
result, err = fetchFunc(ctx, projectKey, nil, nil)
if err != nil {
return nil, err
}
Expand All @@ -67,24 +115,25 @@ func (a apiClientApi) getFlags(ctx context.Context, projectKey string, href *str
if err != nil {
return nil, errors.Wrapf(err, "unable to parse href for next link: %s", *href)
}
featureFlags, _, err = a.apiClient.FeatureFlagsApi.GetFeatureFlags(ctx, projectKey).
Summary(false).
Limit(limit).
Offset(offset).
Execute()
result, err = fetchFunc(ctx, projectKey, &limit, &offset)
if err != nil {
return nil, err
}
}
flags := featureFlags.Items
if next, ok := featureFlags.Links["next"]; ok && next.Href != nil {
newFlags, err := a.getFlags(ctx, projectKey, next.Href)
if err != nil {
return nil, err

items := result.GetItems()

if links := result.GetLinks(); links != nil {
if next, ok := links["next"]; ok && next.Href != nil {
newItems, err := getPaginatedItems(ctx, projectKey, next.Href, fetchFunc)
if err != nil {
return nil, err
}
items = append(items, newItems...)
}
flags = append(flags, newFlags...)
}
return flags, nil

return items, nil
}

func parseHref(href string) (limit, offset int64, err error) {
Expand Down
148 changes: 148 additions & 0 deletions internal/dev_server/adapters/api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package adapters

import (
"context"
"testing"

ldapi "github.com/launchdarkly/api-client-go/v14"
"github.com/stretchr/testify/assert"
)

type testItem struct {
ID string
}

type testResult struct {
items []testItem
links map[string]ldapi.Link
}

func (r testResult) GetItems() []testItem {
return r.items
}

func (r testResult) GetLinks() map[string]ldapi.Link {
return r.links
}

func TestGetPaginatedItems(t *testing.T) {
ctx := context.Background()
projectKey := "test-project"

testCases := []struct {
name string
fetchResponses []testResult
expectedItems []testItem
expectedError bool
}{
{
name: "Single page",
fetchResponses: []testResult{
{
items: []testItem{{ID: "1"}, {ID: "2"}},
links: map[string]ldapi.Link{},
},
},
expectedItems: []testItem{{ID: "1"}, {ID: "2"}},
},
{
name: "Multiple pages",
fetchResponses: []testResult{
{
items: []testItem{{ID: "1"}, {ID: "2"}},
links: map[string]ldapi.Link{
"next": {Href: strPtr("http://example.com?limit=2&offset=2")},
},
},
{
items: []testItem{{ID: "3"}, {ID: "4"}},
links: map[string]ldapi.Link{},
},
},
expectedItems: []testItem{{ID: "1"}, {ID: "2"}, {ID: "3"}, {ID: "4"}},
},
{
name: "Error on second page",
fetchResponses: []testResult{
{
items: []testItem{{ID: "1"}, {ID: "2"}},
links: map[string]ldapi.Link{
"next": {Href: strPtr("http://example.com?limit=2&offset=2")},
},
},
},
expectedError: true,
},
{
name: "Empty response",
fetchResponses: []testResult{
{
items: []testItem{},
links: map[string]ldapi.Link{},
},
},
expectedItems: []testItem{},
},
{
name: "Multiple pages with varying item counts",
fetchResponses: []testResult{
{
items: []testItem{{ID: "1"}, {ID: "2"}, {ID: "3"}},
links: map[string]ldapi.Link{
"next": {Href: strPtr("http://example.com?limit=3&offset=3")},
},
},
{
items: []testItem{{ID: "4"}, {ID: "5"}},
links: map[string]ldapi.Link{
"next": {Href: strPtr("http://example.com?limit=3&offset=5")},
},
},
{
items: []testItem{{ID: "6"}},
links: map[string]ldapi.Link{},
},
},
expectedItems: []testItem{{ID: "1"}, {ID: "2"}, {ID: "3"}, {ID: "4"}, {ID: "5"}, {ID: "6"}},
},
{
name: "Invalid next link",
fetchResponses: []testResult{
{
items: []testItem{{ID: "1"}, {ID: "2"}},
links: map[string]ldapi.Link{
"next": {Href: strPtr("invalid-url")},
},
},
},
expectedError: true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
callCount := 0
fetchFunc := func(ctx context.Context, projectKey string, limit, offset *int64) (testResult, error) {
if callCount >= len(tc.fetchResponses) {
return testResult{}, assert.AnError
}
result := tc.fetchResponses[callCount]
callCount++
return result, nil
}

items, err := getPaginatedItems(ctx, projectKey, nil, fetchFunc)

if tc.expectedError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tc.expectedItems, items)
}
})
}
}

func strPtr(s string) *string {
return &s
}
15 changes: 15 additions & 0 deletions internal/dev_server/adapters/mocks/api.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 32 additions & 0 deletions internal/dev_server/api/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,27 @@ paths:
description: OK. override removed
404:
description: no matching override found
/dev/projects/{projectKey}/environments:
get:
operationId: getProjectsEnvironments
summary: list all environments for the given project
parameters:
- $ref: "#/components/parameters/projectKey"
responses:
200:
description: OK. List of environments
content:
application/json:
schema:
description: list of environments
type: array
items:
$ref: "#/components/schemas/Environment"
uniqueItems: true
404:
$ref: "#/components/responses/ErrorResponse"
400:
$ref: "#/components/responses/ErrorResponse"
components:
parameters:
flagKey:
Expand Down Expand Up @@ -224,6 +245,17 @@ components:
type: integer
x-go-type: int64
description: unix timestamp for the lat time the flag values were synced from the source environment
Environment:
description: Environment
type: object
required:
- key
- name
properties:
key:
type: string
name:
type: string
responses:
FlagOverride:
description: Flag override
Expand Down
Loading
Loading