Skip to content
Open
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
25 changes: 25 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ jobs:
permissions:
contents: write
packages: write
# id-token + attestations are required for Sigstore OIDC + the GitHub
# Artifact Attestations API used by attest-build-provenance.
id-token: write
attestations: write

steps:
- name: Checkout
Expand All @@ -49,6 +53,12 @@ jobs:
check-latest: true
cache: true

# Syft is the SBOM generator GoReleaser shells out to (see the `sboms:`
# block in .goreleaser.yaml). It is not preinstalled on GitHub-hosted
# runners, so download it before goreleaser runs.
- name: Install Syft
uses: anchore/sbom-action/download-syft@v0.24.0

- name: Run GoReleaser (publish)
uses: goreleaser/goreleaser-action@v7
with:
Expand All @@ -60,6 +70,21 @@ jobs:
# Used to create PRs on the Winget repo
WINGET_TOKEN: ${{ secrets.WINGET_TOKEN }}

# Generate SLSA Level 3 build provenance attestations for the release
# archives and the checksums file. Each attestation is signed via Sigstore
# using the workflow's GitHub OIDC identity, recorded in the public Rekor
# transparency log, and surfaced on the repo's Attestations tab. Customers
# verify with `gh attestation verify --repo overmindtech/cli` or
# `cosign verify-blob-attestation`. See
# docs.overmind.tech/docs/cli/verifying-releases.md.
- name: Attest build provenance for release archives
uses: actions/attest-build-provenance@v3
with:
subject-path: |
dist/overmind_cli_*.tar.gz
dist/overmind_cli_*.zip
dist/checksums.txt

- name: Install cloudsmith CLI
run: |
pip install --upgrade cloudsmith-cli
Expand Down
10 changes: 10 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,16 @@ winget:
checksum:
name_template: "checksums.txt"

# Generate one SPDX-format SBOM per release archive using Syft (the GoReleaser
# default). Each SBOM is uploaded as a sibling release asset. See
# docs.overmind.tech/docs/cli/verifying-releases.md for the customer-facing
# verification commands.
sboms:
- id: archive-sbom
artifacts: archive
documents:
- "{{ .ArtifactName }}.spdx.json"

snapshot:
version_template: "{{ if .Version }}{{ $cleanVersion := replace .Version \"kargo/\" \"\" }}{{ incpatch $cleanVersion }}{{ else }}0.0.1{{ end }}-next"

Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,19 @@ overmind --version
infrastructure context, standards, and approved patterns. This command shows the
resolved knowledge directory path, valid files with their metadata, and any
validation warnings for invalid files.

You can specify multiple knowledge directories to layer organizational and
stack-specific knowledge:

```bash
overmind knowledge list \
--knowledge-dir .overmind/knowledge \
--knowledge-dir ./stacks/prod/.overmind/knowledge
```

When the same knowledge file name appears in multiple directories, later directories
override earlier ones. For more details, see the
[Knowledge Files documentation](https://docs.overmind.tech/docs/knowledge/knowledge).

## Cloud Provider Support

Expand Down
79 changes: 62 additions & 17 deletions aws-source/adapters/elb-load-balancer.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package adapters

import (
"context"
"sync"

elb "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing"
"github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing/types"
Expand All @@ -27,36 +28,80 @@ func elbTagsToMap(tags []types.Tag) map[string]string {
return m
}

func elbLoadBalancerOutputMapper(ctx context.Context, client elbClient, scope string, _ *elb.DescribeLoadBalancersInput, output *elb.DescribeLoadBalancersOutput) ([]*sdp.Item, error) {
items := make([]*sdp.Item, 0)
// AWS DescribeTags API limits requests to 20 load balancers per call.
// See: https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_DescribeTags.html
const elbDescribeTagsMaxItems = 20

loadBalancerNames := make([]string, 0)
for _, desc := range output.LoadBalancerDescriptions {
if desc.LoadBalancerName != nil {
loadBalancerNames = append(loadBalancerNames, *desc.LoadBalancerName)
}
func elbGetTagsMap(ctx context.Context, client elbClient, loadBalancerNames []string) map[string][]types.Tag {
tagsMap := make(map[string][]types.Tag)
if len(loadBalancerNames) == 0 {
return tagsMap
}

// Map of load balancer name to tags
tagsMap := make(map[string][]types.Tag)
if len(loadBalancerNames) > 0 {
// Get all tags for all load balancers in this output
tagsOut, err := client.DescribeTags(ctx, &elb.DescribeTagsInput{
LoadBalancerNames: loadBalancerNames,
})
var mu sync.Mutex
var wg sync.WaitGroup

for i := 0; i < len(loadBalancerNames); i += elbDescribeTagsMaxItems {
end := min(i+elbDescribeTagsMaxItems, len(loadBalancerNames))
chunk := loadBalancerNames[i:end]

wg.Add(1)
go func(chunk []string) {
defer wg.Done()

tagsOut, err := client.DescribeTags(ctx, &elb.DescribeTagsInput{
LoadBalancerNames: chunk,
})

mu.Lock()
defer mu.Unlock()

if err != nil {
tags := HandleTagsError(ctx, err)
for _, loadBalancerName := range chunk {
tagsMap[loadBalancerName] = tagsToELBTags(tags)
}
return
}

if err == nil {
for _, tagDesc := range tagsOut.TagDescriptions {
if tagDesc.LoadBalancerName != nil {
tagsMap[*tagDesc.LoadBalancerName] = tagDesc.Tags
}
}
}(chunk)
}

wg.Wait()
return tagsMap
}

func tagsToELBTags(tags map[string]string) []types.Tag {
elbTags := make([]types.Tag, 0, len(tags))
for key, value := range tags {
elbTags = append(elbTags, types.Tag{
Key: &key,
Value: &value,
})
}
return elbTags
}

func elbLoadBalancerOutputMapper(ctx context.Context, client elbClient, scope string, _ *elb.DescribeLoadBalancersInput, output *elb.DescribeLoadBalancersOutput) ([]*sdp.Item, error) {
items := make([]*sdp.Item, 0)

loadBalancerNames := make([]string, 0)
for _, desc := range output.LoadBalancerDescriptions {
if desc.LoadBalancerName != nil {
loadBalancerNames = append(loadBalancerNames, *desc.LoadBalancerName)
}
}

// Map of load balancer name to tags
tagsMap := elbGetTagsMap(ctx, client, loadBalancerNames)

for _, desc := range output.LoadBalancerDescriptions {
attrs, err := ToAttributesWithExclude(desc)

if err != nil {
return nil, err
}
Expand Down Expand Up @@ -184,7 +229,7 @@ func NewELBLoadBalancerAdapter(client elbClient, accountID string, region string
AccountID: accountID,
ItemType: "elb-load-balancer",
AdapterMetadata: elbLoadBalancerAdapterMetadata,
cache: cache,
cache: cache,
DescribeFunc: func(ctx context.Context, client elbClient, input *elb.DescribeLoadBalancersInput) (*elb.DescribeLoadBalancersOutput, error) {
return client.DescribeLoadBalancers(ctx, input)
},
Expand Down
51 changes: 40 additions & 11 deletions aws-source/adapters/elb-load-balancer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package adapters

import (
"context"
"fmt"
"testing"
"time"

Expand All @@ -14,25 +15,54 @@ import (
type mockElbClient struct{}

func (m mockElbClient) DescribeTags(ctx context.Context, params *elb.DescribeTagsInput, optFns ...func(*elb.Options)) (*elb.DescribeTagsOutput, error) {
return &elb.DescribeTagsOutput{
TagDescriptions: []types.TagDescription{
{
LoadBalancerName: new("a8c3c8851f0df43fda89797c8e941a91"),
Tags: []types.Tag{
{
Key: new("foo"),
Value: new("bar"),
},
if len(params.LoadBalancerNames) > elbDescribeTagsMaxItems {
return nil, fmt.Errorf("cannot have more than %v resources described", elbDescribeTagsMaxItems)
}

tagDescriptions := make([]types.TagDescription, 0, len(params.LoadBalancerNames))
for _, name := range params.LoadBalancerNames {
tagDescriptions = append(tagDescriptions, types.TagDescription{
LoadBalancerName: &name,
Tags: []types.Tag{
{
Key: new("foo"),
Value: new("bar"),
},
},
},
})
}

return &elb.DescribeTagsOutput{
TagDescriptions: tagDescriptions,
}, nil
}

func (m mockElbClient) DescribeLoadBalancers(ctx context.Context, params *elb.DescribeLoadBalancersInput, optFns ...func(*elb.Options)) (*elb.DescribeLoadBalancersOutput, error) {
return nil, nil
}

func TestElbGetTagsMapBatching(t *testing.T) {
t.Parallel()

names := make([]string, 0, 25)
for i := range 25 {
names = append(names, fmt.Sprintf("load-balancer-%02d", i))
}

tagsMap := elbGetTagsMap(context.Background(), mockElbClient{}, names)

if len(tagsMap) != 25 {
t.Fatalf("expected 25 tag entries, got %v", len(tagsMap))
}

for _, name := range names {
tags := elbTagsToMap(tagsMap[name])
if tags["foo"] != "bar" {
t.Errorf("expected tag foo for %v to be bar, got %q", name, tags["foo"])
}
}
}

func TestELBv2LoadBalancerOutputMapper(t *testing.T) {
output := &elb.DescribeLoadBalancersOutput{
LoadBalancerDescriptions: []types.LoadBalancerDescription{
Expand Down Expand Up @@ -128,7 +158,6 @@ func TestELBv2LoadBalancerOutputMapper(t *testing.T) {
}

items, err := elbLoadBalancerOutputMapper(context.Background(), mockElbClient{}, "foo", nil, output)

if err != nil {
t.Error(err)
}
Expand Down
54 changes: 38 additions & 16 deletions aws-source/adapters/elbv2.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net/url"
"sync"

elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2"
"github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types"
Expand All @@ -30,32 +31,53 @@ func elbv2TagsToMap(tags []types.Tag) map[string]string {
return m
}

// AWS DescribeTags API limits requests to 20 resources per call.
// See: https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_DescribeTags.html
const elbv2DescribeTagsMaxItems = 20

// Gets a map of ARN to tags (in map[string]string format) for the given ARNs
func elbv2GetTagsMap(ctx context.Context, client elbv2Client, arns []string) map[string]map[string]string {
tagsMap := make(map[string]map[string]string)

if len(arns) > 0 {
tagsOut, err := client.DescribeTags(ctx, &elbv2.DescribeTagsInput{
ResourceArns: arns,
})
if err != nil {
tags := HandleTagsError(ctx, err)
if len(arns) == 0 {
return tagsMap
}

// Set these tags for all ARNs
for _, arn := range arns {
tagsMap[arn] = tags
}
var mu sync.Mutex
var wg sync.WaitGroup

return tagsMap
}
for i := 0; i < len(arns); i += elbv2DescribeTagsMaxItems {
end := min(i+elbv2DescribeTagsMaxItems, len(arns))
chunk := arns[i:end]

wg.Add(1)
go func(chunk []string) {
defer wg.Done()

tagsOut, err := client.DescribeTags(ctx, &elbv2.DescribeTagsInput{
ResourceArns: chunk,
})

for _, tagDescription := range tagsOut.TagDescriptions {
if tagDescription.ResourceArn != nil {
tagsMap[*tagDescription.ResourceArn] = elbv2TagsToMap(tagDescription.Tags)
mu.Lock()
defer mu.Unlock()

if err != nil {
tags := HandleTagsError(ctx, err)
for _, arn := range chunk {
tagsMap[arn] = tags
}
return
}
}

for _, tagDescription := range tagsOut.TagDescriptions {
if tagDescription.ResourceArn != nil {
tagsMap[*tagDescription.ResourceArn] = elbv2TagsToMap(tagDescription.Tags)
}
}
}(chunk)
}

wg.Wait()
return tagsMap
}

Expand Down
Loading