82
.buildkite/pipeline.yml
Normal file
82
.buildkite/pipeline.yml
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
env:
|
||||||
|
ARTIFACT_NAME: bk-infra9-metal-bridge
|
||||||
|
BIN_NAME: infra9-metal-bridge
|
||||||
|
DEPLOYMENT_REPO: k8s-infra9-metal-bridge
|
||||||
|
GOPRIVATE: go.equinixmetal.net/*
|
||||||
|
IMAGE_TAG: ${BUILDKITE_BUILD_NUMBER}-${BUILDKITE_COMMIT:0:8}
|
||||||
|
QUAY_REPO: quay.io/equinixmetal/infra9-metal-bridge
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- label: ":golangci-lint: lint"
|
||||||
|
key: "lint"
|
||||||
|
command: |
|
||||||
|
make lint
|
||||||
|
plugins:
|
||||||
|
- docker#v5.3.0:
|
||||||
|
image: "golangci/golangci-lint:v1.51.2"
|
||||||
|
volumes:
|
||||||
|
- "/var/lib/buildkite-agent/.gitconfig/:/root/.gitconfig/"
|
||||||
|
|
||||||
|
- label: ":test_tube: test"
|
||||||
|
key: "test"
|
||||||
|
command: |
|
||||||
|
make test
|
||||||
|
plugins:
|
||||||
|
- docker#v5.3.0:
|
||||||
|
image: "golang:1.20"
|
||||||
|
volumes:
|
||||||
|
- "/var/lib/buildkite-agent/.gitconfig/:/root/.gitconfig/"
|
||||||
|
|
||||||
|
- label: ":golang: build"
|
||||||
|
key: "gobuild"
|
||||||
|
commands: |
|
||||||
|
CGO_ENABLED=0 go build -o ${ARTIFACT_NAME} -buildvcs=false ./main.go
|
||||||
|
artifact_paths: "${ARTIFACT_NAME}"
|
||||||
|
plugins:
|
||||||
|
- docker#v5.6.0:
|
||||||
|
image: "golang:1.20"
|
||||||
|
volumes:
|
||||||
|
- "/var/lib/buildkite-agent/.gitconfig:/root/.gitconfig"
|
||||||
|
|
||||||
|
- label: ":whale: docker build"
|
||||||
|
key: "build"
|
||||||
|
depends_on: ["lint", "test", "gobuild"]
|
||||||
|
commands: |
|
||||||
|
#!/bin/bash
|
||||||
|
echo --- Retrieve artifacts
|
||||||
|
buildkite-agent artifact download "${ARTIFACT_NAME}" .
|
||||||
|
mv "${ARTIFACT_NAME}" "${BIN_NAME}"
|
||||||
|
|
||||||
|
# make sure it is executable
|
||||||
|
chmod +x ${BIN_NAME}
|
||||||
|
|
||||||
|
echo --- Build Docker Image
|
||||||
|
docker build . -t "$QUAY_REPO:$IMAGE_TAG"
|
||||||
|
|
||||||
|
echo --- Push Docker Image
|
||||||
|
docker push "$QUAY_REPO:$IMAGE_TAG"
|
||||||
|
|
||||||
|
buildkite-agent annotate --style "success" "Image pushed to quay [$QUAY_REPO:$IMAGE_TAG](https://$QUAY_REPO:$IMAGE_TAG)"
|
||||||
|
|
||||||
|
# For main commits, pull-requests will be created to bump the image in the deployment manifest
|
||||||
|
- label: "Bump image tag for main branch builds"
|
||||||
|
depends_on:
|
||||||
|
- "build"
|
||||||
|
if: build.branch == 'main'
|
||||||
|
plugins:
|
||||||
|
- first-aml/git-clone:
|
||||||
|
repository: git@github.com:equinixmetal/$DEPLOYMENT_REPO.git
|
||||||
|
- ssh://git@github.com/equinixmetal/ssm-buildkite-plugin#v1.0.3:
|
||||||
|
parameters:
|
||||||
|
GITHUB_TOKEN: /buildkite/github/personal-access-token/v1
|
||||||
|
- ssh://git@github.com/packethost/yaml-update-buildkite-plugin#v1.0.1:
|
||||||
|
dir: $DEPLOYMENT_REPO
|
||||||
|
file: values.yaml
|
||||||
|
values:
|
||||||
|
- .bridge.image.tag=$IMAGE_TAG
|
||||||
|
- ssh://git@github.com/equinixmetal/github-pr-template-buildkite-plugin#v0.2.0: {}
|
||||||
|
# Create Pull Request to main using commit from previous step
|
||||||
|
- envato/github-pull-request#v0.4.0:
|
||||||
|
title: '[buildkite] bump image tag to $IMAGE_TAG'
|
||||||
|
head: buildkite-yaml-update-$BUILDKITE_BUILD_NUMBER
|
||||||
|
base: main
|
||||||
56
.golangci.yml
Normal file
56
.golangci.yml
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
linters-settings:
|
||||||
|
gofumpt:
|
||||||
|
extra-rules: true
|
||||||
|
|
||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
# default linters
|
||||||
|
- errcheck
|
||||||
|
- gosimple
|
||||||
|
- govet
|
||||||
|
- ineffassign
|
||||||
|
- staticcheck
|
||||||
|
- typecheck
|
||||||
|
- unused
|
||||||
|
|
||||||
|
# additional linters
|
||||||
|
- bodyclose
|
||||||
|
- gocritic
|
||||||
|
- gocyclo
|
||||||
|
# - goerr113
|
||||||
|
- gofmt
|
||||||
|
- goimports
|
||||||
|
- revive
|
||||||
|
- gomnd
|
||||||
|
- govet
|
||||||
|
- misspell
|
||||||
|
- noctx
|
||||||
|
- stylecheck
|
||||||
|
- whitespace
|
||||||
|
- wsl
|
||||||
|
|
||||||
|
# - bod
|
||||||
|
issues:
|
||||||
|
exclude:
|
||||||
|
# Default excludes from `golangci-lint run --help` with EXC0002 removed
|
||||||
|
# EXC0001 errcheck: Almost all programs ignore errors on these functions and in most cases it's ok
|
||||||
|
- Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*print(f|ln)?|os\.(Un)?Setenv). is not checked
|
||||||
|
# EXC0002 golint: Annoying issue about not having a comment. The rare codebase has such comments
|
||||||
|
# - (comment on exported (method|function|type|const)|should have( a package)? comment|comment should be of the form)
|
||||||
|
# EXC0003 golint: False positive when tests are defined in package 'test'
|
||||||
|
- func name will be used as test\.Test.* by other packages, and that stutters; consider calling this
|
||||||
|
# EXC0004 govet: Common false positives
|
||||||
|
- (possible misuse of unsafe.Pointer|should have signature)
|
||||||
|
# EXC0005 staticcheck: Developers tend to write in C-style with an explicit 'break' in a 'switch', so it's ok to ignore
|
||||||
|
- ineffective break statement. Did you mean to break out of the outer loop
|
||||||
|
# EXC0006 gosec: Too many false-positives on 'unsafe' usage
|
||||||
|
- Use of unsafe calls should be audited
|
||||||
|
# EXC0007 gosec: Too many false-positives for parametrized shell calls
|
||||||
|
- Subprocess launch(ed with variable|ing should be audited)
|
||||||
|
# EXC0008 gosec: Duplicated errcheck checks
|
||||||
|
- (G104|G307)
|
||||||
|
# EXC0009 gosec: Too many issues in popular repos
|
||||||
|
- (Expect directory permissions to be 0750 or less|Expect file permissions to be 0600 or less)
|
||||||
|
# EXC0010 gosec: False positive is triggered by 'src, err := ioutil.ReadFile(filename)'
|
||||||
|
- Potential file inclusion via variable
|
||||||
|
exclude-use-default: false
|
||||||
60
Makefile
Normal file
60
Makefile
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
GOPKG=go.equinixmetal.net/infra9-metal-bridge
|
||||||
|
|
||||||
|
ROOT_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
|
||||||
|
TOOLS_DIR := .tools
|
||||||
|
|
||||||
|
GO_FILES=$(shell git ls-files '*.go')
|
||||||
|
|
||||||
|
GOTOOLS_REPO = golang.org/x/tools
|
||||||
|
GOTOOLS_VERSION = v0.9.3
|
||||||
|
|
||||||
|
GOLANGCI_LINT_REPO = github.com/golangci/golangci-lint
|
||||||
|
GOLANGCI_LINT_VERSION = v1.51.2
|
||||||
|
|
||||||
|
.PHONY: all help lint test golint fix-lint unit-test coverage
|
||||||
|
|
||||||
|
|
||||||
|
help: Makefile ## Print help.
|
||||||
|
@grep -h "##" $(MAKEFILE_LIST) | grep -v grep | sed -e 's/:.*##/#/' | column -c 2 -t -s#
|
||||||
|
|
||||||
|
all: lint test ## Lints and tests.
|
||||||
|
|
||||||
|
lint: golint ## Runs all lint checks.
|
||||||
|
|
||||||
|
golint: | $(TOOLS_DIR)/golangci-lint ## Runs Go lint checks.
|
||||||
|
@echo Linting Go files...
|
||||||
|
@$(TOOLS_DIR)/golangci-lint run --timeout=5m
|
||||||
|
|
||||||
|
fix-lint: $(GO_FILES) | $(TOOLS_DIR)/goimports ## Runs goimports on all go files.
|
||||||
|
@echo Linting go files...
|
||||||
|
@$(TOOLS_DIR)/goimports -w -local $(GOPKG) $^
|
||||||
|
|
||||||
|
test: | unit-test ## Regenerate files and run unit tests.
|
||||||
|
|
||||||
|
unit-test: ## Runs unit tests.
|
||||||
|
@echo Running unit tests...
|
||||||
|
@go test -timeout 30s -cover -short ./...
|
||||||
|
|
||||||
|
coverage: ## Generates a test coverage report.
|
||||||
|
@echo Generating coverage report...
|
||||||
|
@go test -timeout 30s ./... -coverprofile=coverage.out -covermode=atomic
|
||||||
|
@go tool cover -func=coverage.out
|
||||||
|
@go tool cover -html=coverage.out
|
||||||
|
|
||||||
|
bin: ## Builds the binary
|
||||||
|
@echo Building binary
|
||||||
|
@go build -o infra9-metal-bridge ./main.go
|
||||||
|
|
||||||
|
# Tools setup
|
||||||
|
$(TOOLS_DIR):
|
||||||
|
mkdir -p $(TOOLS_DIR)
|
||||||
|
|
||||||
|
$(TOOLS_DIR)/goimports: | $(TOOLS_DIR)
|
||||||
|
@echo "Installing $(GOTOOLS_REPO)/cmd/goimports@$(GOTOOLS_VERSION)"
|
||||||
|
@GOBIN=$(ROOT_DIR)/$(TOOLS_DIR) go install $(GOTOOLS_REPO)/cmd/goimports@$(GOTOOLS_VERSION)
|
||||||
|
|
||||||
|
$(TOOLS_DIR)/golangci-lint: | $(TOOLS_DIR)
|
||||||
|
@echo "Installing $(GOLANGCI_LINT_REPO)/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION)"
|
||||||
|
@GOBIN=$(ROOT_DIR)/$(TOOLS_DIR) go install $(GOLANGCI_LINT_REPO)/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION)
|
||||||
|
$@ version
|
||||||
|
$@ linters
|
||||||
32
cmd/serve.go
32
cmd/serve.go
@@ -5,6 +5,7 @@ import (
|
|||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"go.infratographer.com/x/echox"
|
"go.infratographer.com/x/echox"
|
||||||
"go.infratographer.com/x/events"
|
"go.infratographer.com/x/events"
|
||||||
|
"go.infratographer.com/x/oauth2x"
|
||||||
"go.infratographer.com/x/otelx"
|
"go.infratographer.com/x/otelx"
|
||||||
"go.infratographer.com/x/versionx"
|
"go.infratographer.com/x/versionx"
|
||||||
"go.infratographer.com/x/viperx"
|
"go.infratographer.com/x/viperx"
|
||||||
@@ -13,7 +14,6 @@ import (
|
|||||||
"go.equinixmetal.net/infra9-metal-bridge/internal/metal"
|
"go.equinixmetal.net/infra9-metal-bridge/internal/metal"
|
||||||
"go.equinixmetal.net/infra9-metal-bridge/internal/permissions"
|
"go.equinixmetal.net/infra9-metal-bridge/internal/permissions"
|
||||||
"go.equinixmetal.net/infra9-metal-bridge/internal/pubsub"
|
"go.equinixmetal.net/infra9-metal-bridge/internal/pubsub"
|
||||||
"go.equinixmetal.net/infra9-metal-bridge/internal/routes"
|
|
||||||
"go.equinixmetal.net/infra9-metal-bridge/internal/service"
|
"go.equinixmetal.net/infra9-metal-bridge/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -33,9 +33,11 @@ func init() {
|
|||||||
events.MustViperFlagsForSubscriber(viper.GetViper(), serveCmd.Flags())
|
events.MustViperFlagsForSubscriber(viper.GetViper(), serveCmd.Flags())
|
||||||
|
|
||||||
serveCmd.PersistentFlags().StringSlice("events-topics", []string{}, "event topics to subscribe to")
|
serveCmd.PersistentFlags().StringSlice("events-topics", []string{}, "event topics to subscribe to")
|
||||||
viperx.MustBindFlag(viper.GetViper(), "events.topics", serveCmd.PersistentFlags().Lookup("events-topics"))
|
viperx.MustBindFlag(viper.GetViper(), "events.subscriber.topics", serveCmd.PersistentFlags().Lookup("events-topics"))
|
||||||
|
|
||||||
|
oauth2x.MustViperFlags(viper.GetViper(), serveCmd.Flags())
|
||||||
permissions.MustViperFlags(viper.GetViper(), serveCmd.Flags())
|
permissions.MustViperFlags(viper.GetViper(), serveCmd.Flags())
|
||||||
|
metal.MustViperFlags(viper.GetViper(), serveCmd.Flags())
|
||||||
}
|
}
|
||||||
|
|
||||||
func serve(cmd *cobra.Command, _ []string) {
|
func serve(cmd *cobra.Command, _ []string) {
|
||||||
@@ -57,9 +59,21 @@ func serve(cmd *cobra.Command, _ []string) {
|
|||||||
logger.Fatalw("error initializing Metal client", "error", err)
|
logger.Fatalw("error initializing Metal client", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
permHTTPClient := permissions.DefaultHTTPClient
|
||||||
|
|
||||||
|
if config.AppConfig.OIDC.Client.ID != "" {
|
||||||
|
tokenSrc, err := oauth2x.NewClientCredentialsTokenSrc(cmd.Context(), config.AppConfig.OIDC.Client)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatalw("error initializing oauth2 client", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
permHTTPClient = oauth2x.NewClient(cmd.Context(), tokenSrc)
|
||||||
|
}
|
||||||
|
|
||||||
perms, err := permissions.NewClient("",
|
perms, err := permissions.NewClient("",
|
||||||
permissions.WithLogger(logger),
|
permissions.WithLogger(logger),
|
||||||
permissions.WithConfig(config.AppConfig.Permissions),
|
permissions.WithConfig(config.AppConfig.Permissions),
|
||||||
|
permissions.WithHTTPClient(permHTTPClient),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalw("error initializing Permissions client", "error", err)
|
logger.Fatalw("error initializing Permissions client", "error", err)
|
||||||
@@ -74,27 +88,19 @@ func serve(cmd *cobra.Command, _ []string) {
|
|||||||
logger.Fatalw("error initializing service", "error", err)
|
logger.Fatalw("error initializing service", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
subscriber, err := pubsub.NewSubscriber(cmd.Context(), config.AppConfig.Events.Subscriber, service,
|
subscriber, err := pubsub.NewSubscriber(cmd.Context(), config.AppConfig.Events.Subscriber.SubscriberConfig, service,
|
||||||
pubsub.WithLogger(logger),
|
pubsub.WithLogger(logger),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalw("unable to initialize event subscriber", "error", err)
|
logger.Fatalw("unable to initialize event subscriber", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, topic := range viper.GetStringSlice("events.topics") {
|
for _, topic := range config.AppConfig.Events.Subscriber.Topics {
|
||||||
if err := subscriber.Subscribe(topic); err != nil {
|
if err := subscriber.Subscribe(topic); err != nil {
|
||||||
logger.Fatalw("error subscribing to topic: "+topic, "topic", topic, "error", err)
|
logger.Fatalw("error subscribing to topic: "+topic, "topic", topic, "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
router, err := routes.NewRouter(
|
|
||||||
routes.WithLogger(logger.Desugar()),
|
|
||||||
routes.WithService(service),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
logger.Fatalw("error initializing router", "error", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
srv, err := echox.NewServer(
|
srv, err := echox.NewServer(
|
||||||
logger.Desugar(),
|
logger.Desugar(),
|
||||||
echox.ConfigFromViper(viper.GetViper()),
|
echox.ConfigFromViper(viper.GetViper()),
|
||||||
@@ -104,8 +110,6 @@ func serve(cmd *cobra.Command, _ []string) {
|
|||||||
logger.Fatalw("failed to initialize new server", "error", err)
|
logger.Fatalw("failed to initialize new server", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
srv.AddHandler(router)
|
|
||||||
|
|
||||||
defer subscriber.Close()
|
defer subscriber.Close()
|
||||||
|
|
||||||
logger.Info("Listening for events")
|
logger.Info("Listening for events")
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
|
oidc:
|
||||||
|
client:
|
||||||
|
id: idntcli-abc123
|
||||||
|
secret: somesecret
|
||||||
|
issuer: http://mock-oauth2-server:8081/default
|
||||||
|
|
||||||
events:
|
events:
|
||||||
|
subscriber:
|
||||||
topics:
|
topics:
|
||||||
- '*.*'
|
- '*.*'
|
||||||
subscriber:
|
|
||||||
url: nats://nats:4222
|
url: nats://nats:4222
|
||||||
prefix: com.equinixmetal
|
prefix: com.equinixmetal
|
||||||
queueGroup: metal-bridge
|
queueGroup: metal-bridge
|
||||||
|
|||||||
30
go.mod
30
go.mod
@@ -5,13 +5,16 @@ go 1.20
|
|||||||
require (
|
require (
|
||||||
github.com/ThreeDotsLabs/watermill v1.2.0
|
github.com/ThreeDotsLabs/watermill v1.2.0
|
||||||
github.com/labstack/echo/v4 v4.10.2
|
github.com/labstack/echo/v4 v4.10.2
|
||||||
github.com/nats-io/nats.go v1.26.0
|
github.com/nats-io/nats.go v1.27.1
|
||||||
github.com/spf13/cobra v1.7.0
|
github.com/spf13/cobra v1.7.0
|
||||||
|
github.com/spf13/pflag v1.0.5
|
||||||
github.com/spf13/viper v1.16.0
|
github.com/spf13/viper v1.16.0
|
||||||
go.infratographer.com/x v0.3.2
|
github.com/stretchr/testify v1.8.4
|
||||||
|
go.infratographer.com/x v0.3.3
|
||||||
go.opentelemetry.io/otel v1.16.0
|
go.opentelemetry.io/otel v1.16.0
|
||||||
go.opentelemetry.io/otel/trace v1.16.0
|
go.opentelemetry.io/otel/trace v1.16.0
|
||||||
go.uber.org/zap v1.24.0
|
go.uber.org/zap v1.24.0
|
||||||
|
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -20,6 +23,7 @@ require (
|
|||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
|
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||||
github.com/garsue/watermillzap v1.2.0 // indirect
|
github.com/garsue/watermillzap v1.2.0 // indirect
|
||||||
github.com/go-logr/logr v1.2.4 // indirect
|
github.com/go-logr/logr v1.2.4 // indirect
|
||||||
@@ -33,14 +37,14 @@ require (
|
|||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jaevor/go-nanoid v1.3.0 // indirect
|
github.com/jaevor/go-nanoid v1.3.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/compress v1.16.5 // indirect
|
github.com/klauspost/compress v1.16.7 // indirect
|
||||||
github.com/labstack/echo-contrib v0.15.0 // indirect
|
github.com/labstack/echo-contrib v0.15.0 // indirect
|
||||||
github.com/labstack/echo-jwt/v4 v4.2.0 // indirect
|
github.com/labstack/echo-jwt/v4 v4.2.0 // indirect
|
||||||
github.com/labstack/gommon v0.4.0 // indirect
|
github.com/labstack/gommon v0.4.0 // indirect
|
||||||
github.com/lithammer/shortuuid/v3 v3.0.7 // indirect
|
github.com/lithammer/shortuuid/v3 v3.0.7 // indirect
|
||||||
github.com/magiconair/properties v1.8.7 // indirect
|
github.com/magiconair/properties v1.8.7 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.18 // indirect
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
@@ -50,14 +54,15 @@ require (
|
|||||||
github.com/oklog/ulid v1.3.1 // indirect
|
github.com/oklog/ulid v1.3.1 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_golang v1.14.0 // indirect
|
github.com/prometheus/client_golang v1.14.0 // indirect
|
||||||
github.com/prometheus/client_model v0.3.0 // indirect
|
github.com/prometheus/client_model v0.3.0 // indirect
|
||||||
github.com/prometheus/common v0.40.0 // indirect
|
github.com/prometheus/common v0.40.0 // indirect
|
||||||
github.com/prometheus/procfs v0.9.0 // indirect
|
github.com/prometheus/procfs v0.11.0 // indirect
|
||||||
github.com/spf13/afero v1.9.5 // indirect
|
github.com/spf13/afero v1.9.5 // indirect
|
||||||
github.com/spf13/cast v1.5.1 // indirect
|
github.com/spf13/cast v1.5.1 // indirect
|
||||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/stretchr/objx v0.5.0 // indirect
|
||||||
github.com/subosito/gotenv v1.4.2 // indirect
|
github.com/subosito/gotenv v1.4.2 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
@@ -73,15 +78,16 @@ require (
|
|||||||
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
|
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
|
||||||
go.uber.org/atomic v1.10.0 // indirect
|
go.uber.org/atomic v1.10.0 // indirect
|
||||||
go.uber.org/multierr v1.9.0 // indirect
|
go.uber.org/multierr v1.9.0 // indirect
|
||||||
golang.org/x/crypto v0.9.0 // indirect
|
golang.org/x/crypto v0.11.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
|
golang.org/x/net v0.12.0 // indirect
|
||||||
golang.org/x/net v0.10.0 // indirect
|
golang.org/x/oauth2 v0.10.0 // indirect
|
||||||
golang.org/x/sys v0.8.0 // indirect
|
golang.org/x/sys v0.10.0 // indirect
|
||||||
golang.org/x/text v0.9.0 // indirect
|
golang.org/x/text v0.11.0 // indirect
|
||||||
golang.org/x/time v0.3.0 // indirect
|
golang.org/x/time v0.3.0 // indirect
|
||||||
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
|
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
|
||||||
google.golang.org/grpc v1.55.0 // indirect
|
google.golang.org/grpc v1.55.0 // indirect
|
||||||
google.golang.org/protobuf v1.30.0 // indirect
|
google.golang.org/protobuf v1.31.0 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
50
go.sum
50
go.sum
@@ -49,7 +49,7 @@ github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kd
|
|||||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/brianvoe/gofakeit/v6 v6.21.0 h1:tNkm9yxEbpuPK8Bx39tT4sSc5i9SUGiciLdNix+VDQY=
|
github.com/brianvoe/gofakeit/v6 v6.23.0 h1:pgVhyWpYq4e0GEVCh2gdZnS/nBX+8SnyTBliHg5xjks=
|
||||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
@@ -187,8 +187,8 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
|
|||||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
|
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
|
||||||
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
@@ -212,8 +212,8 @@ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxec
|
|||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
|
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||||
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||||
github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g=
|
github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g=
|
||||||
@@ -226,8 +226,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
|||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/nats-io/jwt/v2 v2.4.1 h1:Y35W1dgbbz2SQUYDPCaclXcuqleVmpbRa7646Jf2EX4=
|
github.com/nats-io/jwt/v2 v2.4.1 h1:Y35W1dgbbz2SQUYDPCaclXcuqleVmpbRa7646Jf2EX4=
|
||||||
github.com/nats-io/nats-server/v2 v2.9.17 h1:gFpUQ3hqIDJrnqog+Bl5vaXg+RhhYEZIElasEuRn2tw=
|
github.com/nats-io/nats-server/v2 v2.9.17 h1:gFpUQ3hqIDJrnqog+Bl5vaXg+RhhYEZIElasEuRn2tw=
|
||||||
github.com/nats-io/nats.go v1.26.0 h1:fWJTYPnZ8DzxIaqIHOAMfColuznchnd5Ab5dbJpgPIE=
|
github.com/nats-io/nats.go v1.27.1 h1:OuYnal9aKVSnOzLQIzf7554OXMCG7KbaTkCSBHRcSoo=
|
||||||
github.com/nats-io/nats.go v1.26.0/go.mod h1:XpbWUlOElGwTYbMR7imivs7jJj9GtK7ypv321Wp6pjc=
|
github.com/nats-io/nats.go v1.27.1/go.mod h1:XpbWUlOElGwTYbMR7imivs7jJj9GtK7ypv321Wp6pjc=
|
||||||
github.com/nats-io/nkeys v0.4.4 h1:xvBJ8d69TznjcQl9t6//Q5xXuVhyYiSos6RPtvQNTwA=
|
github.com/nats-io/nkeys v0.4.4 h1:xvBJ8d69TznjcQl9t6//Q5xXuVhyYiSos6RPtvQNTwA=
|
||||||
github.com/nats-io/nkeys v0.4.4/go.mod h1:XUkxdLPTufzlihbamfzQ7mw/VGx6ObUs+0bN5sNvt64=
|
github.com/nats-io/nkeys v0.4.4/go.mod h1:XUkxdLPTufzlihbamfzQ7mw/VGx6ObUs+0bN5sNvt64=
|
||||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||||
@@ -248,8 +248,8 @@ github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvq
|
|||||||
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
|
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
|
||||||
github.com/prometheus/common v0.40.0 h1:Afz7EVRqGg2Mqqf4JuF9vdvp1pi220m55Pi9T2JnO4Q=
|
github.com/prometheus/common v0.40.0 h1:Afz7EVRqGg2Mqqf4JuF9vdvp1pi220m55Pi9T2JnO4Q=
|
||||||
github.com/prometheus/common v0.40.0/go.mod h1:L65ZJPSmfn/UBWLQIHV7dBrKFidB/wPlF1y5TlSt9OE=
|
github.com/prometheus/common v0.40.0/go.mod h1:L65ZJPSmfn/UBWLQIHV7dBrKFidB/wPlF1y5TlSt9OE=
|
||||||
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
|
github.com/prometheus/procfs v0.11.0 h1:5EAgkfkMl659uZPbe9AS2N68a7Cc1TJbPEuGzFuRbyk=
|
||||||
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
|
github.com/prometheus/procfs v0.11.0/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
|
||||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
@@ -280,6 +280,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
|||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
|
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
|
||||||
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
@@ -291,8 +292,8 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
|||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
go.infratographer.com/x v0.3.2 h1:AxHY77AGhWcRNcO7ENP/4Cj0xg6KGKpxFn/yZn+rPOs=
|
go.infratographer.com/x v0.3.3 h1:dTaLEp75RgL0JxKJhrcuQTP4a2x/MrevvZ3OdtkEhCs=
|
||||||
go.infratographer.com/x v0.3.2/go.mod h1:GvOhGwi/1Dp5qAQSudHUdLfFmiXzzc27KBfkH0nxnEQ=
|
go.infratographer.com/x v0.3.3/go.mod h1:pXXSdeJBisAK3AdED5EFj7Yo8z8td7fOWDkNl4Dkp0s=
|
||||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
@@ -339,8 +340,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
|||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
||||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
@@ -351,8 +352,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
|||||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
|
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw=
|
||||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
||||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
@@ -409,8 +410,8 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v
|
|||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
@@ -421,7 +422,8 @@ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ
|
|||||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8=
|
golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
|
||||||
|
golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -475,8 +477,8 @@ golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
@@ -487,8 +489,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
@@ -647,8 +649,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
|
|||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"go.infratographer.com/x/echox"
|
"go.infratographer.com/x/echox"
|
||||||
"go.infratographer.com/x/events"
|
"go.infratographer.com/x/events"
|
||||||
"go.infratographer.com/x/loggingx"
|
"go.infratographer.com/x/loggingx"
|
||||||
|
"go.infratographer.com/x/oauth2x"
|
||||||
"go.infratographer.com/x/otelx"
|
"go.infratographer.com/x/otelx"
|
||||||
|
|
||||||
"go.equinixmetal.net/infra9-metal-bridge/internal/metal"
|
"go.equinixmetal.net/infra9-metal-bridge/internal/metal"
|
||||||
@@ -15,6 +16,7 @@ import (
|
|||||||
// AppConfig is the main application configuration.
|
// AppConfig is the main application configuration.
|
||||||
var AppConfig struct {
|
var AppConfig struct {
|
||||||
Logging loggingx.Config
|
Logging loggingx.Config
|
||||||
|
OIDC OIDCClientConfig
|
||||||
EquinixMetal metal.Config
|
EquinixMetal metal.Config
|
||||||
OTel otelx.Config
|
OTel otelx.Config
|
||||||
Server echox.Config
|
Server echox.Config
|
||||||
@@ -27,5 +29,16 @@ var AppConfig struct {
|
|||||||
// EventsConfig defines the configuration setting up both subscriptions and publishing
|
// EventsConfig defines the configuration setting up both subscriptions and publishing
|
||||||
type EventsConfig struct {
|
type EventsConfig struct {
|
||||||
Publisher events.PublisherConfig
|
Publisher events.PublisherConfig
|
||||||
Subscriber events.SubscriberConfig
|
Subscriber SubscriberConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// OIDCClientConfig defines the configuration for OIDC Client Credentials.
|
||||||
|
type OIDCClientConfig struct {
|
||||||
|
Client oauth2x.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscriberConfig extends events SubscriberConfig by adding topics.
|
||||||
|
type SubscriberConfig struct {
|
||||||
|
events.SubscriberConfig `mapstructure:",squash"`
|
||||||
|
Topics []string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package metal
|
package metal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers/emapi"
|
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers/emapi"
|
||||||
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers/emgql"
|
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers/emgql"
|
||||||
)
|
)
|
||||||
@@ -13,3 +16,9 @@ type Config struct {
|
|||||||
// EMAPI sets the provider to Equinix Metal API.
|
// EMAPI sets the provider to Equinix Metal API.
|
||||||
EMAPI emapi.Config
|
EMAPI emapi.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MustViperFlags registers command flags along with the viper bindings.
|
||||||
|
func MustViperFlags(v *viper.Viper, flags *pflag.FlagSet) {
|
||||||
|
emgql.MustViperFlags(v, flags)
|
||||||
|
emapi.MustViperFlags(v, flags)
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,4 +5,7 @@ import "errors"
|
|||||||
var (
|
var (
|
||||||
// ErrUnauthorized is returned when the token provided did not validate to a user.
|
// ErrUnauthorized is returned when the token provided did not validate to a user.
|
||||||
ErrUnauthorized = errors.New("unauthorized key")
|
ErrUnauthorized = errors.New("unauthorized key")
|
||||||
|
|
||||||
|
// ErrMetalProviderRequired is returned when no provider has been configured for the metal client.
|
||||||
|
ErrMetalProviderRequired = errors.New("metal provider required")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,34 +3,53 @@ package metal
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/models"
|
|
||||||
provider "go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers"
|
|
||||||
"go.infratographer.com/x/gidx"
|
"go.infratographer.com/x/gidx"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/models"
|
||||||
|
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Client is the Equinix Metal API Client struct.
|
// Client is the Equinix Metal Client Interface.
|
||||||
type Client struct {
|
type Client interface {
|
||||||
logger *zap.Logger
|
providers.Provider
|
||||||
|
|
||||||
provider provider.Provider
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetOrganizationDetails(ctx context.Context, id gidx.PrefixedID) (*models.OrganizationDetails, error) {
|
// client is the Equinix Metal Client struct.
|
||||||
|
type client struct {
|
||||||
|
logger *zap.Logger
|
||||||
|
|
||||||
|
provider providers.Provider
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOrganizationDetails fetches the organization id provided with its memberships.
|
||||||
|
func (c *client) GetOrganizationDetails(ctx context.Context, id gidx.PrefixedID) (*models.OrganizationDetails, error) {
|
||||||
return c.provider.GetOrganizationDetails(ctx, id)
|
return c.provider.GetOrganizationDetails(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetProjectDetails(ctx context.Context, id gidx.PrefixedID) (*models.ProjectDetails, error) {
|
// GetProjectDetails fetchs the provided project id with membership information.
|
||||||
|
func (c *client) GetProjectDetails(ctx context.Context, id gidx.PrefixedID) (*models.ProjectDetails, error) {
|
||||||
return c.provider.GetProjectDetails(ctx, id)
|
return c.provider.GetProjectDetails(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetUserDetails(ctx context.Context, id gidx.PrefixedID) (*models.UserDetails, error) {
|
// GetUserDetails fetches the provided user id.
|
||||||
|
func (c *client) GetUserDetails(ctx context.Context, id gidx.PrefixedID) (*models.UserDetails, error) {
|
||||||
return c.provider.GetUserDetails(ctx, id)
|
return c.provider.GetUserDetails(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new Client.
|
// GetUserOrganizationRole returns the role for the user in the organization.
|
||||||
func New(options ...Option) (*Client, error) {
|
func (c *client) GetUserOrganizationRole(ctx context.Context, userID, orgID gidx.PrefixedID) (string, error) {
|
||||||
client := new(Client)
|
return c.provider.GetUserOrganizationRole(ctx, userID, orgID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserProjectRole returns the role for the user in the project.
|
||||||
|
func (c *client) GetUserProjectRole(ctx context.Context, userID, projID gidx.PrefixedID) (string, error) {
|
||||||
|
return c.provider.GetUserProjectRole(ctx, userID, projID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new Equinix Metal Client.
|
||||||
|
func New(options ...Option) (Client, error) {
|
||||||
|
client := new(client)
|
||||||
|
|
||||||
for _, opt := range options {
|
for _, opt := range options {
|
||||||
if err := opt(client); err != nil {
|
if err := opt(client); err != nil {
|
||||||
@@ -42,5 +61,9 @@ func New(options ...Option) (*Client, error) {
|
|||||||
client.logger = zap.NewNop()
|
client.logger = zap.NewNop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if client.provider == nil {
|
||||||
|
return nil, ErrMetalProviderRequired
|
||||||
|
}
|
||||||
|
|
||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|||||||
2
internal/metal/models/doc.go
Normal file
2
internal/metal/models/doc.go
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// Package models defines generic models each provider must be able to return.
|
||||||
|
package models
|
||||||
@@ -9,4 +9,7 @@ const (
|
|||||||
|
|
||||||
// IDPrefixUser defines the ID Prefix for a User.
|
// IDPrefixUser defines the ID Prefix for a User.
|
||||||
IDPrefixUser = "metlusr"
|
IDPrefixUser = "metlusr"
|
||||||
|
|
||||||
|
// IdentityPrefixUser defines the ID Prefix for a User created with Identity API.
|
||||||
|
IdentityPrefixUser = "idntusr"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
|
// Membership contains metal membership details.
|
||||||
type Membership[T any] struct {
|
type Membership[T any] struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
User *UserDetails `json:"user"`
|
User *UserDetails `json:"user"`
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package models
|
|||||||
|
|
||||||
import "go.infratographer.com/x/gidx"
|
import "go.infratographer.com/x/gidx"
|
||||||
|
|
||||||
|
// OrganizationDetails contains the organization and membership information.
|
||||||
type OrganizationDetails struct {
|
type OrganizationDetails struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -9,6 +10,7 @@ type OrganizationDetails struct {
|
|||||||
Projects []*ProjectDetails `json:"projects"`
|
Projects []*ProjectDetails `json:"projects"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PrefixedID returns the prefixed id for the organization.
|
||||||
func (d *OrganizationDetails) PrefixedID() gidx.PrefixedID {
|
func (d *OrganizationDetails) PrefixedID() gidx.PrefixedID {
|
||||||
if d.ID == "" {
|
if d.ID == "" {
|
||||||
return gidx.NullPrefixedID
|
return gidx.NullPrefixedID
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package models
|
|||||||
|
|
||||||
import "go.infratographer.com/x/gidx"
|
import "go.infratographer.com/x/gidx"
|
||||||
|
|
||||||
|
// ProjectDetails contains project and membership information.
|
||||||
type ProjectDetails struct {
|
type ProjectDetails struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -9,6 +10,7 @@ type ProjectDetails struct {
|
|||||||
Organization *OrganizationDetails `json:"organization"`
|
Organization *OrganizationDetails `json:"organization"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PrefixedID returns the prefixed id for the project.
|
||||||
func (d *ProjectDetails) PrefixedID() gidx.PrefixedID {
|
func (d *ProjectDetails) PrefixedID() gidx.PrefixedID {
|
||||||
if d.ID == "" {
|
if d.ID == "" {
|
||||||
return gidx.NullPrefixedID
|
return gidx.NullPrefixedID
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import "go.infratographer.com/x/gidx"
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
|
||||||
const (
|
"go.infratographer.com/x/gidx"
|
||||||
MetalUserPrefix = "metlusr"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// MetalUserIssuer is the issuer that is used for metal api token users.
|
||||||
|
MetalUserIssuer = "https://auth.equinix.com/"
|
||||||
|
|
||||||
|
// MetalUserIssuerIDPrefix is the issuer id prefix added by the issuer.
|
||||||
|
MetalUserIssuerIDPrefix = "auth0|"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserDetails contains the user information.
|
||||||
type UserDetails struct {
|
type UserDetails struct {
|
||||||
|
id *gidx.PrefixedID
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
FullName string `json:"full_name"`
|
FullName string `json:"full_name"`
|
||||||
Organizations []*OrganizationDetails `json:"organizations"`
|
Organizations []*OrganizationDetails `json:"organizations"`
|
||||||
@@ -14,10 +25,40 @@ type UserDetails struct {
|
|||||||
Roles []string `json:"roles"`
|
Roles []string `json:"roles"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PrefixedID returns the identity prefixed id for the user.
|
||||||
func (d *UserDetails) PrefixedID() gidx.PrefixedID {
|
func (d *UserDetails) PrefixedID() gidx.PrefixedID {
|
||||||
if d.ID == "" {
|
if d.id != nil {
|
||||||
return gidx.NullPrefixedID
|
return *d.id
|
||||||
}
|
}
|
||||||
|
|
||||||
return gidx.PrefixedID(IDPrefixUser + "-" + d.ID)
|
nullID := gidx.NullPrefixedID
|
||||||
|
|
||||||
|
d.id = &nullID
|
||||||
|
|
||||||
|
if d.ID == "" {
|
||||||
|
return nullID
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := GenerateSubjectID(IdentityPrefixUser, MetalUserIssuer, MetalUserIssuerIDPrefix+d.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nullID
|
||||||
|
}
|
||||||
|
|
||||||
|
d.id = &id
|
||||||
|
|
||||||
|
return *d.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateSubjectID builds a identity prefixed id with the provided prefix for the issuer and subject.
|
||||||
|
func GenerateSubjectID(prefix, iss, sub string) (gidx.PrefixedID, error) {
|
||||||
|
// Concatenate the iss and sub values, then hash them
|
||||||
|
issSub := iss + sub
|
||||||
|
issSubHash := sha256.Sum256([]byte(issSub))
|
||||||
|
|
||||||
|
digest := base64.RawURLEncoding.EncodeToString(issSubHash[:])
|
||||||
|
|
||||||
|
// Concatenate the prefix with the digest
|
||||||
|
out := prefix + "-" + digest
|
||||||
|
|
||||||
|
return gidx.Parse(out)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
package metal
|
package metal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
provider "go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers"
|
||||||
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers/emapi"
|
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers/emapi"
|
||||||
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers/emgql"
|
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers/emgql"
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Option is a Client configuration Option definition.
|
// Option is a Client configuration Option definition.
|
||||||
type Option func(c *Client) error
|
type Option func(c *client) error
|
||||||
|
|
||||||
// WithProvider sets the provider on the client.
|
// WithProvider sets the provider on the client.
|
||||||
func WithProvider(provider provider.Provider) Option {
|
func WithProvider(provider providers.Provider) Option {
|
||||||
return func(c *Client) error {
|
return func(c *client) error {
|
||||||
c.provider = provider
|
c.provider = provider
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -21,7 +22,7 @@ func WithProvider(provider provider.Provider) Option {
|
|||||||
|
|
||||||
// WithLogger sets the logger for the client.
|
// WithLogger sets the logger for the client.
|
||||||
func WithLogger(logger *zap.Logger) Option {
|
func WithLogger(logger *zap.Logger) Option {
|
||||||
return func(c *Client) error {
|
return func(c *client) error {
|
||||||
c.logger = logger
|
c.logger = logger
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -30,7 +31,7 @@ func WithLogger(logger *zap.Logger) Option {
|
|||||||
|
|
||||||
// WithConfig applies all configurations defined in the config.
|
// WithConfig applies all configurations defined in the config.
|
||||||
func WithConfig(config Config) Option {
|
func WithConfig(config Config) Option {
|
||||||
return func(c *Client) error {
|
return func(c *client) error {
|
||||||
var options []Option
|
var options []Option
|
||||||
|
|
||||||
if config.EMGQL.Populated() {
|
if config.EMGQL.Populated() {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
provider "go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers"
|
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -25,12 +25,14 @@ const (
|
|||||||
staffHeaderValue = "true"
|
staffHeaderValue = "true"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DefaultHTTPClient is the default http client used if no client is provided.
|
||||||
var DefaultHTTPClient = &http.Client{
|
var DefaultHTTPClient = &http.Client{
|
||||||
Timeout: defaultHTTPTimeout,
|
Timeout: defaultHTTPTimeout,
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ provider.Provider = &Client{}
|
var _ providers.Provider = &Client{}
|
||||||
|
|
||||||
|
// Client is the client to interact with the equinix metal api.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
logger *zap.SugaredLogger
|
logger *zap.SugaredLogger
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
@@ -39,6 +41,8 @@ type Client struct {
|
|||||||
consumerToken string
|
consumerToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Do executes the provided request.
|
||||||
|
// If the out value is provided, the response will attempt to be json decoded.
|
||||||
func (c *Client) Do(req *http.Request, out any) (*http.Response, error) {
|
func (c *Client) Do(req *http.Request, out any) (*http.Response, error) {
|
||||||
if c.authToken != "" {
|
if c.authToken != "" {
|
||||||
req.Header.Set(authHeader, c.authToken)
|
req.Header.Set(authHeader, c.authToken)
|
||||||
@@ -68,6 +72,7 @@ func (c *Client) Do(req *http.Request, out any) (*http.Response, error) {
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DoRequest creates a new request from the provided parameters and executes the request.
|
||||||
func (c *Client) DoRequest(ctx context.Context, method, path string, body io.Reader, out any) (*http.Response, error) {
|
func (c *Client) DoRequest(ctx context.Context, method, path string, body io.Reader, out any) (*http.Response, error) {
|
||||||
path = strings.TrimPrefix(path, c.baseURL.Path)
|
path = strings.TrimPrefix(path, c.baseURL.Path)
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ package emapi
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"go.infratographer.com/x/viperx"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config provides configuration for connecting to the Equinix Metal API provider.
|
// Config provides configuration for connecting to the Equinix Metal API provider.
|
||||||
@@ -17,6 +21,19 @@ type Config struct {
|
|||||||
ConsumerToken string
|
ConsumerToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MustViperFlags registers command flags along with the viper bindings.
|
||||||
|
func MustViperFlags(v *viper.Viper, flags *pflag.FlagSet) {
|
||||||
|
flags.String("emapi-base-url", "", "Equinix Metal Rest API Base URL")
|
||||||
|
viperx.MustBindFlag(v, "equinixmetal.emapi.baseurl", flags.Lookup("emapi-base-url"))
|
||||||
|
|
||||||
|
flags.String("emapi-auth-token", "", "Equinix Metal Rest Auth Token")
|
||||||
|
viperx.MustBindFlag(v, "equinixmetal.emapi.authtoken", flags.Lookup("emapi-auth-token"))
|
||||||
|
|
||||||
|
flags.String("emapi-consumer-token", "", "Equinix Metal Rest Consumer Token")
|
||||||
|
viperx.MustBindFlag(v, "equinixmetal.emapi.consumertoken", flags.Lookup("emapi-consumer-token"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populated checks if any field has been populated.
|
||||||
func (c Config) Populated() bool {
|
func (c Config) Populated() bool {
|
||||||
return c.AuthToken != "" || c.ConsumerToken != "" || c.BaseURL != ""
|
return c.AuthToken != "" || c.ConsumerToken != "" || c.BaseURL != ""
|
||||||
}
|
}
|
||||||
|
|||||||
2
internal/metal/providers/emapi/doc.go
Normal file
2
internal/metal/providers/emapi/doc.go
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// Package emapi implement a metal provider which fetches details from the Equinix Metal API.
|
||||||
|
package emapi
|
||||||
@@ -2,4 +2,5 @@ package emapi
|
|||||||
|
|
||||||
import "errors"
|
import "errors"
|
||||||
|
|
||||||
|
// ErrBaseURLRequired is returned if no base url is provided.
|
||||||
var ErrBaseURLRequired = errors.New("emapi base url required")
|
var ErrBaseURLRequired = errors.New("emapi base url required")
|
||||||
|
|||||||
27
internal/metal/providers/emapi/helpers.go
Normal file
27
internal/metal/providers/emapi/helpers.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package emapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// idOrLinkID returns the id if not empty, otherwise it plucks the last subpath from the link.
|
||||||
|
// An empty string is returned if nothing is defined or an error occurs.
|
||||||
|
func idOrLinkID(id, link string) string {
|
||||||
|
if id != "" || link == "" {
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
url, err := url.Parse(link)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(strings.TrimRight(url.Path, "/"), "/")
|
||||||
|
|
||||||
|
if len(parts) != 0 {
|
||||||
|
return parts[len(parts)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
@@ -4,10 +4,13 @@ import (
|
|||||||
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/models"
|
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Roles contains a list of roles.
|
||||||
type Roles []string
|
type Roles []string
|
||||||
|
|
||||||
|
// Memberships contains a list of memberships
|
||||||
type Memberships []*Membership
|
type Memberships []*Membership
|
||||||
|
|
||||||
|
// ToDetailsWithOrganizationDetails converts the memberships to generic membership models with organization details.
|
||||||
func (m Memberships) ToDetailsWithOrganizationDetails(orgDetails *models.OrganizationDetails) []*models.Membership[models.OrganizationDetails] {
|
func (m Memberships) ToDetailsWithOrganizationDetails(orgDetails *models.OrganizationDetails) []*models.Membership[models.OrganizationDetails] {
|
||||||
memberships := make([]*models.Membership[models.OrganizationDetails], len(m))
|
memberships := make([]*models.Membership[models.OrganizationDetails], len(m))
|
||||||
|
|
||||||
@@ -28,6 +31,7 @@ func (m Memberships) ToDetailsWithOrganizationDetails(orgDetails *models.Organiz
|
|||||||
return memberships
|
return memberships
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToDetailsWithProjectDetails converts the memberships to generic membership models with project details.
|
||||||
func (m Memberships) ToDetailsWithProjectDetails(projDetails *models.ProjectDetails) []*models.Membership[models.ProjectDetails] {
|
func (m Memberships) ToDetailsWithProjectDetails(projDetails *models.ProjectDetails) []*models.Membership[models.ProjectDetails] {
|
||||||
memberships := make([]*models.Membership[models.ProjectDetails], len(m))
|
memberships := make([]*models.Membership[models.ProjectDetails], len(m))
|
||||||
|
|
||||||
@@ -48,15 +52,15 @@ func (m Memberships) ToDetailsWithProjectDetails(projDetails *models.ProjectDeta
|
|||||||
return memberships
|
return memberships
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Membership contains membership information.
|
||||||
type Membership struct {
|
type Membership struct {
|
||||||
client *Client
|
|
||||||
|
|
||||||
HREF string `json:"href"`
|
HREF string `json:"href"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Roles Roles `json:"roles"`
|
Roles Roles `json:"roles"`
|
||||||
User *User `json:"user"`
|
User *User `json:"user"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToDetailsWithOrganizationDetails converts the membership to generic membership model with organization details.
|
||||||
func (m *Membership) ToDetailsWithOrganizationDetails(orgDetails *models.OrganizationDetails) *models.Membership[models.OrganizationDetails] {
|
func (m *Membership) ToDetailsWithOrganizationDetails(orgDetails *models.OrganizationDetails) *models.Membership[models.OrganizationDetails] {
|
||||||
if m.ID == "" {
|
if m.ID == "" {
|
||||||
return nil
|
return nil
|
||||||
@@ -70,6 +74,7 @@ func (m *Membership) ToDetailsWithOrganizationDetails(orgDetails *models.Organiz
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToDetailsWithProjectDetails converts the membership to generic membership model with organization details.
|
||||||
func (m *Membership) ToDetailsWithProjectDetails(projDetails *models.ProjectDetails) *models.Membership[models.ProjectDetails] {
|
func (m *Membership) ToDetailsWithProjectDetails(projDetails *models.ProjectDetails) *models.Membership[models.ProjectDetails] {
|
||||||
if m.ID == "" {
|
if m.ID == "" {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -14,8 +14,10 @@ const (
|
|||||||
organizationsPath = "/organizations"
|
organizationsPath = "/organizations"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Organizations contains a list of organizations.
|
||||||
type Organizations []*Organization
|
type Organizations []*Organization
|
||||||
|
|
||||||
|
// ToDetails converts to a generic model organization details.
|
||||||
func (o Organizations) ToDetails() []*models.OrganizationDetails {
|
func (o Organizations) ToDetails() []*models.OrganizationDetails {
|
||||||
orgs := make([]*models.OrganizationDetails, len(o))
|
orgs := make([]*models.OrganizationDetails, len(o))
|
||||||
|
|
||||||
@@ -36,9 +38,8 @@ func (o Organizations) ToDetails() []*models.OrganizationDetails {
|
|||||||
return orgs
|
return orgs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Organization contains organization information.
|
||||||
type Organization struct {
|
type Organization struct {
|
||||||
client *Client
|
|
||||||
|
|
||||||
HREF string `json:"href"`
|
HREF string `json:"href"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -47,13 +48,20 @@ type Organization struct {
|
|||||||
Projects Projects `json:"projects"`
|
Projects Projects `json:"projects"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToDetails converts the object to a generic orgnization details.
|
||||||
func (o *Organization) ToDetails() *models.OrganizationDetails {
|
func (o *Organization) ToDetails() *models.OrganizationDetails {
|
||||||
if o == nil || o.ID == "" {
|
var id string
|
||||||
|
|
||||||
|
if o != nil {
|
||||||
|
id = idOrLinkID(o.ID, o.HREF)
|
||||||
|
}
|
||||||
|
|
||||||
|
if id == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
details := &models.OrganizationDetails{
|
details := &models.OrganizationDetails{
|
||||||
ID: o.ID,
|
ID: id,
|
||||||
Name: o.Name,
|
Name: o.Name,
|
||||||
Projects: o.Projects.ToDetails(),
|
Projects: o.Projects.ToDetails(),
|
||||||
}
|
}
|
||||||
@@ -63,10 +71,11 @@ func (o *Organization) ToDetails() *models.OrganizationDetails {
|
|||||||
return details
|
return details
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getOrganizationWithMemberships fetches an organization from the equinix metal api with membership user information.
|
||||||
func (c *Client) getOrganizationWithMemberships(ctx context.Context, id string) (*Organization, error) {
|
func (c *Client) getOrganizationWithMemberships(ctx context.Context, id string) (*Organization, error) {
|
||||||
var org Organization
|
var org Organization
|
||||||
|
|
||||||
_, err := c.DoRequest(ctx, http.MethodGet, organizationsPath+"/"+id+"?include=memberships.user,projects.memberships.user", nil, &org)
|
_, err := c.DoRequest(ctx, http.MethodGet, organizationsPath+"/"+id+"?include=memberships.user", nil, &org) // nolint:bodyclose // body is closed by Do json decode.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error loading organization: %w", err)
|
return nil, fmt.Errorf("error loading organization: %w", err)
|
||||||
}
|
}
|
||||||
@@ -74,6 +83,7 @@ func (c *Client) getOrganizationWithMemberships(ctx context.Context, id string)
|
|||||||
return &org, nil
|
return &org, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetOrganizationDetails fetches the organization id provided with its memberships.
|
||||||
func (c *Client) GetOrganizationDetails(ctx context.Context, id gidx.PrefixedID) (*models.OrganizationDetails, error) {
|
func (c *Client) GetOrganizationDetails(ctx context.Context, id gidx.PrefixedID) (*models.OrganizationDetails, error) {
|
||||||
org, err := c.getOrganizationWithMemberships(ctx, id.String()[gidx.PrefixPartLength+1:])
|
org, err := c.getOrganizationWithMemberships(ctx, id.String()[gidx.PrefixPartLength+1:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -14,8 +14,10 @@ const (
|
|||||||
projectsPath = "/projects"
|
projectsPath = "/projects"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Projects contains a list of projects.
|
||||||
type Projects []*Project
|
type Projects []*Project
|
||||||
|
|
||||||
|
// ToDetails converts the objects to generic project details.
|
||||||
func (p Projects) ToDetails() []*models.ProjectDetails {
|
func (p Projects) ToDetails() []*models.ProjectDetails {
|
||||||
projects := make([]*models.ProjectDetails, len(p))
|
projects := make([]*models.ProjectDetails, len(p))
|
||||||
|
|
||||||
@@ -36,9 +38,8 @@ func (p Projects) ToDetails() []*models.ProjectDetails {
|
|||||||
return projects
|
return projects
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Project contains project information.
|
||||||
type Project struct {
|
type Project struct {
|
||||||
client *Client
|
|
||||||
|
|
||||||
HREF string `json:"href"`
|
HREF string `json:"href"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -47,13 +48,20 @@ type Project struct {
|
|||||||
Organization *Organization `json:"organization"`
|
Organization *Organization `json:"organization"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToDetails converts the project to generic project details.
|
||||||
func (p *Project) ToDetails() *models.ProjectDetails {
|
func (p *Project) ToDetails() *models.ProjectDetails {
|
||||||
if p == nil || p.ID == "" {
|
var id string
|
||||||
|
|
||||||
|
if p != nil {
|
||||||
|
id = idOrLinkID(p.ID, p.HREF)
|
||||||
|
}
|
||||||
|
|
||||||
|
if id == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
details := &models.ProjectDetails{
|
details := &models.ProjectDetails{
|
||||||
ID: p.ID,
|
ID: id,
|
||||||
Name: p.Name,
|
Name: p.Name,
|
||||||
Organization: p.Organization.ToDetails(),
|
Organization: p.Organization.ToDetails(),
|
||||||
}
|
}
|
||||||
@@ -63,19 +71,21 @@ func (p *Project) ToDetails() *models.ProjectDetails {
|
|||||||
return details
|
return details
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) getProject(ctx context.Context, id string) (*Project, error) {
|
// getProjectWithMemberships fetches the provided project with membership information.
|
||||||
|
func (c *Client) getProjectWithMemberships(ctx context.Context, id string) (*Project, error) {
|
||||||
var project Project
|
var project Project
|
||||||
|
|
||||||
_, err := c.DoRequest(ctx, http.MethodGet, c.baseURL.JoinPath(projectsPath, id).String(), nil, &project)
|
_, err := c.DoRequest(ctx, http.MethodGet, projectsPath+"/"+id+"?include=memberships.user", nil, &project) // nolint:bodyclose // body is closed by Do json decode.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error loading project: %w", err)
|
return nil, fmt.Errorf("error loading organization: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &project, nil
|
return &project, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetProjectDetails fetchs the provided project id with membership information.
|
||||||
func (c *Client) GetProjectDetails(ctx context.Context, id gidx.PrefixedID) (*models.ProjectDetails, error) {
|
func (c *Client) GetProjectDetails(ctx context.Context, id gidx.PrefixedID) (*models.ProjectDetails, error) {
|
||||||
project, err := c.getProject(ctx, id.String()[gidx.PrefixPartLength+1:])
|
project, err := c.getProjectWithMemberships(ctx, id.String()[gidx.PrefixPartLength+1:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,10 @@ const (
|
|||||||
usersPath = "/users"
|
usersPath = "/users"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Users contains a list of users.
|
||||||
type Users []*User
|
type Users []*User
|
||||||
|
|
||||||
|
// ToDetails converts the objects to generic user details.
|
||||||
func (u Users) ToDetails() []*models.UserDetails {
|
func (u Users) ToDetails() []*models.UserDetails {
|
||||||
users := make([]*models.UserDetails, len(u))
|
users := make([]*models.UserDetails, len(u))
|
||||||
|
|
||||||
@@ -36,9 +38,8 @@ func (u Users) ToDetails() []*models.UserDetails {
|
|||||||
return users
|
return users
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// User contains user information.
|
||||||
type User struct {
|
type User struct {
|
||||||
client *Client
|
|
||||||
|
|
||||||
HREF string `json:"href"`
|
HREF string `json:"href"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
FullName string `json:"full_name"`
|
FullName string `json:"full_name"`
|
||||||
@@ -46,30 +47,39 @@ type User struct {
|
|||||||
Projects Projects `json:"projects"`
|
Projects Projects `json:"projects"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToDetails converts the user to generic user details.
|
||||||
func (u *User) ToDetails() *models.UserDetails {
|
func (u *User) ToDetails() *models.UserDetails {
|
||||||
if u.ID == "" {
|
var id string
|
||||||
|
|
||||||
|
if u != nil {
|
||||||
|
id = idOrLinkID(u.ID, u.HREF)
|
||||||
|
}
|
||||||
|
|
||||||
|
if id == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return &models.UserDetails{
|
return &models.UserDetails{
|
||||||
ID: u.ID,
|
ID: id,
|
||||||
FullName: u.FullName,
|
FullName: u.FullName,
|
||||||
Organizations: nil,
|
Organizations: nil,
|
||||||
Projects: u.Projects.ToDetails(),
|
Projects: u.Projects.ToDetails(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getUser fetches the provided user.
|
||||||
func (c *Client) getUser(ctx context.Context, id string) (*User, error) {
|
func (c *Client) getUser(ctx context.Context, id string) (*User, error) {
|
||||||
var user User
|
var user User
|
||||||
|
|
||||||
_, err := c.DoRequest(ctx, http.MethodGet, c.baseURL.JoinPath(usersPath, id).String(), nil, &user)
|
_, err := c.DoRequest(ctx, http.MethodGet, usersPath+"/"+id, nil, &user) // nolint:bodyclose // body is closed by Do json decode.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error loading user: %w", err)
|
return nil, fmt.Errorf("error loading organization: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUserDetails fetches the provided user id.
|
||||||
func (c *Client) GetUserDetails(ctx context.Context, id gidx.PrefixedID) (*models.UserDetails, error) {
|
func (c *Client) GetUserDetails(ctx context.Context, id gidx.PrefixedID) (*models.UserDetails, error) {
|
||||||
user, err := c.getUser(ctx, id.String()[gidx.PrefixPartLength+1:])
|
user, err := c.getUser(ctx, id.String()[gidx.PrefixPartLength+1:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -78,3 +88,13 @@ func (c *Client) GetUserDetails(ctx context.Context, id gidx.PrefixedID) (*model
|
|||||||
|
|
||||||
return user.ToDetails(), nil
|
return user.ToDetails(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUserOrganizationRole returns collaborator for all organizations.
|
||||||
|
func (c *Client) GetUserOrganizationRole(ctx context.Context, userID, orgID gidx.PrefixedID) (string, error) {
|
||||||
|
return "collaborator", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserProjectRole returns collaborator for all projects.
|
||||||
|
func (c *Client) GetUserProjectRole(ctx context.Context, userID, projectID gidx.PrefixedID) (string, error) {
|
||||||
|
return "collaborator", nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,22 +5,23 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
provider "go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers"
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultHTTPTimeout = 5 * time.Second
|
defaultHTTPTimeout = 5 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
// DefaultHTTPClient is the default http client used if no client is provided.
|
||||||
DefaultHTTPClient = &http.Client{
|
var DefaultHTTPClient = &http.Client{
|
||||||
Timeout: defaultHTTPTimeout,
|
Timeout: defaultHTTPTimeout,
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
var _ provider.Provider = &Client{}
|
var _ providers.Provider = &Client{}
|
||||||
|
|
||||||
|
// Client is the client to interact with the equinix metal graphql service.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
logger *zap.SugaredLogger
|
logger *zap.SugaredLogger
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
|
|||||||
@@ -1,12 +1,24 @@
|
|||||||
package emgql
|
package emgql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"go.infratographer.com/x/viperx"
|
||||||
|
)
|
||||||
|
|
||||||
// Config provides configuration for connecting to the Equinix Metal API provider.
|
// Config provides configuration for connecting to the Equinix Metal API provider.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
|
||||||
// BaseURL is the baseurl to use when connecting to the Equinix Metal API Provider.
|
// BaseURL is the baseurl to use when connecting to the Equinix Metal API Provider.
|
||||||
BaseURL string
|
BaseURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MustViperFlags registers command flags along with the viper bindings.
|
||||||
|
func MustViperFlags(v *viper.Viper, flags *pflag.FlagSet) {
|
||||||
|
flags.String("emgql-base-url", "", "Equinix Metal GraphQL Base URL")
|
||||||
|
viperx.MustBindFlag(v, "equinixmetal.emgql.baseurl", flags.Lookup("emgql-base-url"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populated checks if any field has been populated.
|
||||||
func (c Config) Populated() bool {
|
func (c Config) Populated() bool {
|
||||||
return c.BaseURL != ""
|
return c.BaseURL != ""
|
||||||
}
|
}
|
||||||
|
|||||||
2
internal/metal/providers/emgql/doc.go
Normal file
2
internal/metal/providers/emgql/doc.go
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// Package emgql implements a metal provider which fetches details from the Equinix Metal GraphQL.
|
||||||
|
package emgql
|
||||||
@@ -3,10 +3,12 @@ package emgql
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/models"
|
|
||||||
"go.infratographer.com/x/gidx"
|
"go.infratographer.com/x/gidx"
|
||||||
|
|
||||||
|
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// GetOrganizationDetails fetches the organization id provided with its memberships.
|
||||||
func (c *Client) GetOrganizationDetails(ctx context.Context, id gidx.PrefixedID) (*models.OrganizationDetails, error) {
|
func (c *Client) GetOrganizationDetails(ctx context.Context, id gidx.PrefixedID) (*models.OrganizationDetails, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ package emgql
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/models"
|
|
||||||
"go.infratographer.com/x/gidx"
|
"go.infratographer.com/x/gidx"
|
||||||
|
|
||||||
|
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// GetProjectDetails fetchs the provided project id with membership information.
|
||||||
func (c *Client) GetProjectDetails(ctx context.Context, id gidx.PrefixedID) (*models.ProjectDetails, error) {
|
func (c *Client) GetProjectDetails(ctx context.Context, id gidx.PrefixedID) (*models.ProjectDetails, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,22 @@ package emgql
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/models"
|
|
||||||
"go.infratographer.com/x/gidx"
|
"go.infratographer.com/x/gidx"
|
||||||
|
|
||||||
|
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// GetUserDetails fetches the provided user id.
|
||||||
func (c *Client) GetUserDetails(ctx context.Context, id gidx.PrefixedID) (*models.UserDetails, error) {
|
func (c *Client) GetUserDetails(ctx context.Context, id gidx.PrefixedID) (*models.UserDetails, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUserOrganizationRole returns collaborator for all organizations.
|
||||||
|
func (c *Client) GetUserOrganizationRole(ctx context.Context, userID, orgID gidx.PrefixedID) (string, error) {
|
||||||
|
return "collaborator", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserProjectRole returns collaborator for all projects.
|
||||||
|
func (c *Client) GetUserProjectRole(ctx context.Context, userID, projID gidx.PrefixedID) (string, error) {
|
||||||
|
return "collaborator", nil
|
||||||
|
}
|
||||||
|
|||||||
52
internal/metal/providers/mock.go
Normal file
52
internal/metal/providers/mock.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"go.infratographer.com/x/gidx"
|
||||||
|
|
||||||
|
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Provider = &MockProvider{}
|
||||||
|
|
||||||
|
// MockProvider implements Provider used for testing.
|
||||||
|
type MockProvider struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOrganizationDetails implements Provider used for testing.
|
||||||
|
func (p *MockProvider) GetOrganizationDetails(ctx context.Context, id gidx.PrefixedID) (*models.OrganizationDetails, error) {
|
||||||
|
args := p.Called(id)
|
||||||
|
|
||||||
|
return args.Get(0).(*models.OrganizationDetails), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProjectDetails implements Provider used for testing.
|
||||||
|
func (p *MockProvider) GetProjectDetails(ctx context.Context, id gidx.PrefixedID) (*models.ProjectDetails, error) {
|
||||||
|
args := p.Called(id)
|
||||||
|
|
||||||
|
return args.Get(0).(*models.ProjectDetails), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserDetails implements Provider used for testing.
|
||||||
|
func (p *MockProvider) GetUserDetails(ctx context.Context, id gidx.PrefixedID) (*models.UserDetails, error) {
|
||||||
|
args := p.Called(id)
|
||||||
|
|
||||||
|
return args.Get(0).(*models.UserDetails), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserOrganizationRole implements Provider used for testing.
|
||||||
|
func (p *MockProvider) GetUserOrganizationRole(ctx context.Context, userID, orgID gidx.PrefixedID) (string, error) {
|
||||||
|
args := p.Called(userID, orgID)
|
||||||
|
|
||||||
|
return args.String(0), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserProjectRole implements Provider used for testing.
|
||||||
|
func (p *MockProvider) GetUserProjectRole(ctx context.Context, userID, projID gidx.PrefixedID) (string, error) {
|
||||||
|
args := p.Called(userID, projID)
|
||||||
|
|
||||||
|
return args.String(0), args.Error(1)
|
||||||
|
}
|
||||||
@@ -1,14 +1,19 @@
|
|||||||
package provider
|
// Package providers defines the provider interface for fetching metal resources.
|
||||||
|
package providers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/models"
|
|
||||||
"go.infratographer.com/x/gidx"
|
"go.infratographer.com/x/gidx"
|
||||||
|
|
||||||
|
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Provider defines the provider implementation.
|
||||||
type Provider interface {
|
type Provider interface {
|
||||||
GetOrganizationDetails(ctx context.Context, id gidx.PrefixedID) (*models.OrganizationDetails, error)
|
GetOrganizationDetails(ctx context.Context, id gidx.PrefixedID) (*models.OrganizationDetails, error)
|
||||||
GetProjectDetails(ctx context.Context, id gidx.PrefixedID) (*models.ProjectDetails, error)
|
GetProjectDetails(ctx context.Context, id gidx.PrefixedID) (*models.ProjectDetails, error)
|
||||||
GetUserDetails(ctx context.Context, id gidx.PrefixedID) (*models.UserDetails, error)
|
GetUserDetails(ctx context.Context, id gidx.PrefixedID) (*models.UserDetails, error)
|
||||||
|
GetUserOrganizationRole(ctx context.Context, userID, orgID gidx.PrefixedID) (string, error)
|
||||||
|
GetUserProjectRole(ctx context.Context, userID, projID gidx.PrefixedID) (string, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,22 +8,26 @@ import (
|
|||||||
"go.infratographer.com/x/gidx"
|
"go.infratographer.com/x/gidx"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// RoleAssign is the role assignment request body.
|
||||||
type RoleAssign struct {
|
type RoleAssign struct {
|
||||||
SubjectID string `json:"subject_id"`
|
SubjectID string `json:"subject_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RoleAssignResponse is the response from a role assignment.
|
||||||
type RoleAssignResponse struct {
|
type RoleAssignResponse struct {
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// roleAssignmentData is the response from listing a role assignment
|
||||||
type roleAssignmentData struct {
|
type roleAssignmentData struct {
|
||||||
Data []struct {
|
Data []struct {
|
||||||
SubjectID string `json:"subject_id"`
|
SubjectID string `json:"subject_id"`
|
||||||
} `json:"data"`
|
} `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) AssignRole(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) error {
|
// AssignRole assigns the provided member ID to the given role ID.
|
||||||
path := fmt.Sprintf("/api/v1/roles/%s/assignments", roleID.String())
|
func (c *client) AssignRole(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) error {
|
||||||
|
path := fmt.Sprintf(permsPathRoleAssignmentsFormat, roleID.String())
|
||||||
|
|
||||||
body, err := encodeJSON(RoleAssign{
|
body, err := encodeJSON(RoleAssign{
|
||||||
SubjectID: memberID.String(),
|
SubjectID: memberID.String(),
|
||||||
@@ -34,7 +38,7 @@ func (c *Client) AssignRole(ctx context.Context, roleID gidx.PrefixedID, memberI
|
|||||||
|
|
||||||
var response RoleAssignResponse
|
var response RoleAssignResponse
|
||||||
|
|
||||||
if _, err = c.DoRequest(ctx, http.MethodPost, path, body, &response); err != nil {
|
if _, err = c.DoRequest(ctx, http.MethodPost, path, body, &response); err != nil { // nolint:bodyclose // body is closed by Do json decode.
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,12 +49,37 @@ func (c *Client) AssignRole(ctx context.Context, roleID gidx.PrefixedID, memberI
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) ListRoleAssignments(ctx context.Context, roleID gidx.PrefixedID) ([]gidx.PrefixedID, error) {
|
// UnassignRole removes the provided member ID from the given role ID.
|
||||||
path := fmt.Sprintf("/api/v1/roles/%s/assignments", roleID.String())
|
func (c *client) UnassignRole(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) error {
|
||||||
|
path := fmt.Sprintf(permsPathRoleAssignmentsFormat, roleID.String())
|
||||||
|
|
||||||
|
body, err := encodeJSON(RoleAssign{
|
||||||
|
SubjectID: memberID.String(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var response RoleAssignResponse
|
||||||
|
|
||||||
|
if _, err = c.DoRequest(ctx, http.MethodDelete, path, body, &response); err != nil { // nolint:bodyclose // body is closed by Do json decode.
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !response.Success {
|
||||||
|
return ErrUnassignmentFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListRoleAssignments lists all assignments for the given role.
|
||||||
|
func (c *client) ListRoleAssignments(ctx context.Context, roleID gidx.PrefixedID) ([]gidx.PrefixedID, error) {
|
||||||
|
path := fmt.Sprintf(permsPathRoleAssignmentsFormat, roleID.String())
|
||||||
|
|
||||||
var response roleAssignmentData
|
var response roleAssignmentData
|
||||||
|
|
||||||
if _, err := c.DoRequest(ctx, http.MethodGet, path, nil, &response); err != nil {
|
if _, err := c.DoRequest(ctx, http.MethodGet, path, nil, &response); err != nil { // nolint:bodyclose // body is closed by Do json decode.
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +97,8 @@ func (c *Client) ListRoleAssignments(ctx context.Context, roleID gidx.PrefixedID
|
|||||||
return assignments, nil
|
return assignments, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) RoleHasAssignment(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) (bool, error) {
|
// RoleHasAssignment gets the assignments for the given role and check for the provided member id.
|
||||||
|
func (c *client) RoleHasAssignment(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) (bool, error) {
|
||||||
assignments, err := c.ListRoleAssignments(ctx, roleID)
|
assignments, err := c.ListRoleAssignments(ctx, roleID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
|
|||||||
@@ -12,16 +12,46 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
"go.infratographer.com/x/gidx"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultPermissionsURL = "https://permissions-api.hollow-a.sv15.metalkube.net"
|
const (
|
||||||
|
defaultHTTPClientTimeout = 5 * time.Second
|
||||||
|
|
||||||
var defaultHTTPClient = &http.Client{
|
defaultPermissionsURL = "https://permissions-api.hollow-a.sv15.metalkube.net"
|
||||||
Timeout: 5 * time.Second,
|
permsPathAllow = "/api/v1/allow"
|
||||||
|
|
||||||
|
permsPathResourceRelationshipsFormat = "/api/v1/resources/%s/relationships"
|
||||||
|
permsPathResourceRelationshipsFrom = "/api/v1/relationships/from/"
|
||||||
|
permsPathResourceRelationshipsTo = "/api/v1/relationships/to/"
|
||||||
|
permsPathResourceRolesFormat = "/api/v1/resources/%s/roles"
|
||||||
|
|
||||||
|
permsPathRoleAssignmentsFormat = "/api/v1/roles/%s/assignments"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultHTTPClient is the default HTTP client for the Permissions Client.
|
||||||
|
var DefaultHTTPClient = &http.Client{
|
||||||
|
Timeout: defaultHTTPClientTimeout,
|
||||||
}
|
}
|
||||||
|
|
||||||
type Client struct {
|
// Client defines the Permissions API client interface.
|
||||||
|
type Client interface {
|
||||||
|
AssignRole(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) error
|
||||||
|
CreateRole(ctx context.Context, resourceID gidx.PrefixedID, actions []string) (gidx.PrefixedID, error)
|
||||||
|
DeleteResourceRelationship(ctx context.Context, resourceID gidx.PrefixedID, relation string, relatedResourceID gidx.PrefixedID) error
|
||||||
|
DeleteRole(ctx context.Context, roleID gidx.PrefixedID) error
|
||||||
|
FindResourceRoleByActions(ctx context.Context, resourceID gidx.PrefixedID, actions []string) (ResourceRole, error)
|
||||||
|
ListResourceRelationshipsFrom(ctx context.Context, resourceID gidx.PrefixedID) ([]ResourceRelationship, error)
|
||||||
|
ListResourceRelationshipsTo(ctx context.Context, resourceID gidx.PrefixedID) ([]ResourceRelationship, error)
|
||||||
|
ListResourceRoles(ctx context.Context, resourceID gidx.PrefixedID) (ResourceRoles, error)
|
||||||
|
ListRoleAssignments(ctx context.Context, roleID gidx.PrefixedID) ([]gidx.PrefixedID, error)
|
||||||
|
RoleHasAssignment(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) (bool, error)
|
||||||
|
UnassignRole(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// client is the permissions client.
|
||||||
|
type client struct {
|
||||||
logger *zap.SugaredLogger
|
logger *zap.SugaredLogger
|
||||||
|
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
@@ -32,7 +62,9 @@ type Client struct {
|
|||||||
allowURL *url.URL
|
allowURL *url.URL
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Do(req *http.Request, out any) (*http.Response, error) {
|
// Do executes the provided request.
|
||||||
|
// If the out value is provided, the response will attempt to be json decoded.
|
||||||
|
func (c *client) Do(req *http.Request, out any) (*http.Response, error) {
|
||||||
if c.token != "" {
|
if c.token != "" {
|
||||||
req.Header.Set(echo.HeaderAuthorization, "Bearer "+c.token)
|
req.Header.Set(echo.HeaderAuthorization, "Bearer "+c.token)
|
||||||
}
|
}
|
||||||
@@ -55,7 +87,8 @@ func (c *Client) Do(req *http.Request, out any) (*http.Response, error) {
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) DoRequest(ctx context.Context, method, path string, body io.Reader, out any) (*http.Response, error) {
|
// DoRequest creates a new request from the provided parameters and executes the request.
|
||||||
|
func (c *client) DoRequest(ctx context.Context, method, path string, body io.Reader, out any) (*http.Response, error) {
|
||||||
path = strings.TrimPrefix(path, c.baseURL.Path)
|
path = strings.TrimPrefix(path, c.baseURL.Path)
|
||||||
|
|
||||||
pathURL, err := url.Parse(path)
|
pathURL, err := url.Parse(path)
|
||||||
@@ -100,10 +133,11 @@ func encodeJSON(v any) (*bytes.Buffer, error) {
|
|||||||
return &buff, nil
|
return &buff, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(token string, options ...Option) (*Client, error) {
|
// NewClient creats a new permissions client.
|
||||||
client := &Client{
|
func NewClient(token string, options ...Option) (Client, error) {
|
||||||
|
client := &client{
|
||||||
logger: zap.NewNop().Sugar(),
|
logger: zap.NewNop().Sugar(),
|
||||||
httpClient: defaultHTTPClient,
|
httpClient: DefaultHTTPClient,
|
||||||
token: token,
|
token: token,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ type Config struct {
|
|||||||
BearerToken string
|
BearerToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MustViperFlags registers command flags along with the viper bindings.
|
||||||
func MustViperFlags(v *viper.Viper, flags *pflag.FlagSet) {
|
func MustViperFlags(v *viper.Viper, flags *pflag.FlagSet) {
|
||||||
flags.String("permissions-baseurl", "", "permissions base url")
|
flags.String("permissions-baseurl", "", "permissions base url")
|
||||||
viperx.MustBindFlag(v, "permissions.baseurl", flags.Lookup("permissions-baseurl"))
|
viperx.MustBindFlag(v, "permissions.baseurl", flags.Lookup("permissions-baseurl"))
|
||||||
@@ -28,7 +29,7 @@ func MustViperFlags(v *viper.Viper, flags *pflag.FlagSet) {
|
|||||||
|
|
||||||
// WithConfig applies all configurations defined in the config.
|
// WithConfig applies all configurations defined in the config.
|
||||||
func WithConfig(config Config) Option {
|
func WithConfig(config Config) Option {
|
||||||
return func(c *Client) error {
|
return func(c *client) error {
|
||||||
var options []Option
|
var options []Option
|
||||||
|
|
||||||
if config.BaseURL != "" {
|
if config.BaseURL != "" {
|
||||||
@@ -51,7 +52,7 @@ func WithConfig(config Config) Option {
|
|||||||
|
|
||||||
// WithBaseURL updates the baseurl used by the client.
|
// WithBaseURL updates the baseurl used by the client.
|
||||||
func WithBaseURL(baseURL string) Option {
|
func WithBaseURL(baseURL string) Option {
|
||||||
return func(c *Client) error {
|
return func(c *client) error {
|
||||||
u, err := url.Parse(baseURL)
|
u, err := url.Parse(baseURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to parse emapi base url %s: %w", baseURL, err)
|
return fmt.Errorf("failed to parse emapi base url %s: %w", baseURL, err)
|
||||||
@@ -65,7 +66,7 @@ func WithBaseURL(baseURL string) Option {
|
|||||||
|
|
||||||
// WithBearerToken sets the bearer token to authenticate the request with.
|
// WithBearerToken sets the bearer token to authenticate the request with.
|
||||||
func WithBearerToken(token string) Option {
|
func WithBearerToken(token string) Option {
|
||||||
return func(c *Client) error {
|
return func(c *client) error {
|
||||||
c.token = token
|
c.token = token
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
2
internal/permissions/doc.go
Normal file
2
internal/permissions/doc.go
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// Package permissions implements a Permissions API client for fetching and manipulating relationships and role assignments.
|
||||||
|
package permissions
|
||||||
@@ -3,6 +3,18 @@ package permissions
|
|||||||
import "errors"
|
import "errors"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
// ErrRoleNotFound is returned when no role is found for a given list of actions.
|
||||||
ErrRoleNotFound = errors.New("role not found")
|
ErrRoleNotFound = errors.New("role not found")
|
||||||
|
|
||||||
|
// ErrAssignmentFailed is returned when a user assignment to a role fails.
|
||||||
ErrAssignmentFailed = errors.New("assignment failed")
|
ErrAssignmentFailed = errors.New("assignment failed")
|
||||||
|
|
||||||
|
// ErrUnassignmentFailed is returned when a user assignment is removed from a role fails.
|
||||||
|
ErrUnassignmentFailed = errors.New("unassignment failed")
|
||||||
|
|
||||||
|
// ErrUnexpectedRoleDeleteFailed is returned when an unknown error is returned when deleting a role.
|
||||||
|
ErrUnexpectedRoleDeleteFailed = errors.New("unknown role delete error")
|
||||||
|
|
||||||
|
// ErrUnexpectedRelationshipDeleteFailed is returned when an unknown error is returned when deleting a relationship.
|
||||||
|
ErrUnexpectedRelationshipDeleteFailed = errors.New("unknown relationship delete error")
|
||||||
)
|
)
|
||||||
|
|||||||
90
internal/permissions/mock.go
Normal file
90
internal/permissions/mock.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package permissions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"go.infratographer.com/x/gidx"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockClient implements Client for testing.
|
||||||
|
type MockClient struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssignRole implements Client for testing.
|
||||||
|
func (c *MockClient) AssignRole(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) error {
|
||||||
|
args := c.Called(roleID, memberID)
|
||||||
|
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateRole implements Client for testing.
|
||||||
|
func (c *MockClient) CreateRole(ctx context.Context, resourceID gidx.PrefixedID, actions []string) (gidx.PrefixedID, error) {
|
||||||
|
args := c.Called(resourceID, actions)
|
||||||
|
|
||||||
|
return args.Get(0).(gidx.PrefixedID), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteResourceRelationship implements Client for testing.
|
||||||
|
func (c *MockClient) DeleteResourceRelationship(ctx context.Context, resourceID gidx.PrefixedID, relation string, relatedResourceID gidx.PrefixedID) error {
|
||||||
|
args := c.Called(resourceID, relation, relatedResourceID)
|
||||||
|
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteRole implements Client for testing.
|
||||||
|
func (c *MockClient) DeleteRole(ctx context.Context, roleID gidx.PrefixedID) error {
|
||||||
|
args := c.Called(roleID)
|
||||||
|
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindResourceRoleByActions implements Client for testing.
|
||||||
|
func (c *MockClient) FindResourceRoleByActions(ctx context.Context, resourceID gidx.PrefixedID, actions []string) (ResourceRole, error) {
|
||||||
|
args := c.Called(resourceID, actions)
|
||||||
|
|
||||||
|
return args.Get(0).(ResourceRole), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListResourceRelationshipsFrom implements Client for testing.
|
||||||
|
func (c *MockClient) ListResourceRelationshipsFrom(ctx context.Context, resourceID gidx.PrefixedID) ([]ResourceRelationship, error) {
|
||||||
|
args := c.Called(resourceID)
|
||||||
|
|
||||||
|
return args.Get(0).([]ResourceRelationship), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListResourceRelationshipsTo implements Client for testing.
|
||||||
|
func (c *MockClient) ListResourceRelationshipsTo(ctx context.Context, resourceID gidx.PrefixedID) ([]ResourceRelationship, error) {
|
||||||
|
args := c.Called(resourceID)
|
||||||
|
|
||||||
|
return args.Get(0).([]ResourceRelationship), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListResourceRoles implements Client for testing.
|
||||||
|
func (c *MockClient) ListResourceRoles(ctx context.Context, resourceID gidx.PrefixedID) (ResourceRoles, error) {
|
||||||
|
args := c.Called(resourceID)
|
||||||
|
|
||||||
|
return args.Get(0).(ResourceRoles), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListRoleAssignments implements Client for testing.
|
||||||
|
func (c *MockClient) ListRoleAssignments(ctx context.Context, roleID gidx.PrefixedID) ([]gidx.PrefixedID, error) {
|
||||||
|
args := c.Called(roleID)
|
||||||
|
|
||||||
|
return args.Get(0).([]gidx.PrefixedID), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoleHasAssignment implements Client for testing.
|
||||||
|
func (c *MockClient) RoleHasAssignment(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) (bool, error) {
|
||||||
|
args := c.Called(roleID, memberID)
|
||||||
|
|
||||||
|
return args.Bool(0), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnassignRole implements Client for testing.
|
||||||
|
func (c *MockClient) UnassignRole(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) error {
|
||||||
|
args := c.Called(roleID, memberID)
|
||||||
|
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
@@ -1,15 +1,28 @@
|
|||||||
package permissions
|
package permissions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Option func(*Client) error
|
// Option is a client configuration option definition.
|
||||||
|
type Option func(*client) error
|
||||||
|
|
||||||
|
// WithLogger sets the logger for the client.
|
||||||
func WithLogger(logger *zap.SugaredLogger) Option {
|
func WithLogger(logger *zap.SugaredLogger) Option {
|
||||||
return func(c *Client) error {
|
return func(c *client) error {
|
||||||
c.logger = logger
|
c.logger = logger
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithHTTPClient sets the http client to be used by the client.
|
||||||
|
func WithHTTPClient(httpClient *http.Client) Option {
|
||||||
|
return func(c *client) error {
|
||||||
|
c.httpClient = httpClient
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
112
internal/permissions/relationships.go
Normal file
112
internal/permissions/relationships.go
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
package permissions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"go.infratographer.com/x/gidx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type resourceRelationship struct {
|
||||||
|
ResourceID string `json:"resource_id"`
|
||||||
|
Relation string `json:"relation"`
|
||||||
|
SubjectID string `json:"subject_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceRelationship defines the resource to subject relationship.
|
||||||
|
type ResourceRelationship struct {
|
||||||
|
ResourceID gidx.PrefixedID
|
||||||
|
Relation string
|
||||||
|
SubjectID gidx.PrefixedID
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceRelationshipRequest defines the request to relate to a subject.
|
||||||
|
type ResourceRelationshipRequest struct {
|
||||||
|
Relation string `json:"relation"`
|
||||||
|
SubjectID string `json:"subject_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceRelationshipDeleteResponse defines the response for a delete of a relationship.
|
||||||
|
type ResourceRelationshipDeleteResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteResourceRelationship deletes the provided resources relationship to the given subject id.
|
||||||
|
func (c *client) DeleteResourceRelationship(ctx context.Context, resourceID gidx.PrefixedID, relation string, relatedResourceID gidx.PrefixedID) error {
|
||||||
|
path := fmt.Sprintf(permsPathResourceRelationshipsFormat, resourceID.String())
|
||||||
|
|
||||||
|
body, err := encodeJSON(ResourceRelationshipRequest{
|
||||||
|
Relation: relation,
|
||||||
|
SubjectID: relatedResourceID.String(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var response ResourceRelationshipDeleteResponse
|
||||||
|
|
||||||
|
if _, err := c.DoRequest(ctx, http.MethodDelete, path, body, &response); err != nil { // nolint:bodyclose // closed by Do on json decode.
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !response.Success {
|
||||||
|
return ErrUnexpectedRelationshipDeleteFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListResourceRelationshipsFrom returns resources related to the given id.
|
||||||
|
func (c *client) ListResourceRelationshipsFrom(ctx context.Context, resourceID gidx.PrefixedID) ([]ResourceRelationship, error) {
|
||||||
|
var response struct {
|
||||||
|
Data []resourceRelationship `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := c.DoRequest(ctx, http.MethodGet, permsPathResourceRelationshipsFrom+resourceID.String(), nil, &response); err != nil { // nolint:bodyclose // closed by Do on json decode.
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make([]ResourceRelationship, len(response.Data))
|
||||||
|
|
||||||
|
for i, entry := range response.Data {
|
||||||
|
subID, err := gidx.Parse(entry.SubjectID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data[i] = ResourceRelationship{
|
||||||
|
Relation: entry.Relation,
|
||||||
|
SubjectID: subID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListResourceRelationshipsTo returns resources related to the given id.
|
||||||
|
func (c *client) ListResourceRelationshipsTo(ctx context.Context, resourceID gidx.PrefixedID) ([]ResourceRelationship, error) {
|
||||||
|
var response struct {
|
||||||
|
Data []resourceRelationship `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := c.DoRequest(ctx, http.MethodGet, permsPathResourceRelationshipsTo+resourceID.String(), nil, &response); err != nil { // nolint:bodyclose // closed by Do on json decode.
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make([]ResourceRelationship, len(response.Data))
|
||||||
|
|
||||||
|
for i, entry := range response.Data {
|
||||||
|
resID, err := gidx.Parse(entry.ResourceID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data[i] = ResourceRelationship{
|
||||||
|
ResourceID: resID,
|
||||||
|
Relation: entry.Relation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
@@ -9,23 +9,33 @@ import (
|
|||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ResourceRoleCreate is the role create request.
|
||||||
type ResourceRoleCreate struct {
|
type ResourceRoleCreate struct {
|
||||||
Actions []string `json:"actions"`
|
Actions []string `json:"actions"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResourceRoleCreateResponse is the role creation response.
|
||||||
type ResourceRoleCreateResponse struct {
|
type ResourceRoleCreateResponse struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResourceRoleDeleteResponse is the role deletion response.
|
||||||
|
type ResourceRoleDeleteResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceRoles is a listg of resource roles.
|
||||||
type ResourceRoles []ResourceRole
|
type ResourceRoles []ResourceRole
|
||||||
|
|
||||||
|
// ResourceRole contains the role id and its actions.
|
||||||
type ResourceRole struct {
|
type ResourceRole struct {
|
||||||
ID gidx.PrefixedID `json:"id"`
|
ID gidx.PrefixedID `json:"id"`
|
||||||
Actions []string `json:"actions"`
|
Actions []string `json:"actions"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) CreateRole(ctx context.Context, resourceID gidx.PrefixedID, actions []string) (gidx.PrefixedID, error) {
|
// CreateRole creates a role on the given resource id with the provided actions.
|
||||||
path := fmt.Sprintf("/api/v1/resources/%s/roles", resourceID.String())
|
func (c *client) CreateRole(ctx context.Context, resourceID gidx.PrefixedID, actions []string) (gidx.PrefixedID, error) {
|
||||||
|
path := fmt.Sprintf(permsPathResourceRolesFormat, resourceID.String())
|
||||||
|
|
||||||
body, err := encodeJSON(ResourceRoleCreate{
|
body, err := encodeJSON(ResourceRoleCreate{
|
||||||
Actions: actions,
|
Actions: actions,
|
||||||
@@ -36,7 +46,7 @@ func (c *Client) CreateRole(ctx context.Context, resourceID gidx.PrefixedID, act
|
|||||||
|
|
||||||
var response ResourceRoleCreateResponse
|
var response ResourceRoleCreateResponse
|
||||||
|
|
||||||
if _, err = c.DoRequest(ctx, http.MethodPost, path, body, &response); err != nil {
|
if _, err = c.DoRequest(ctx, http.MethodPost, path, body, &response); err != nil { // nolint:bodyclose // closed by Do on json decode.
|
||||||
return gidx.NullPrefixedID, err
|
return gidx.NullPrefixedID, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,19 +58,40 @@ func (c *Client) CreateRole(ctx context.Context, resourceID gidx.PrefixedID, act
|
|||||||
return roleID, nil
|
return roleID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) ListResourceRoles(ctx context.Context, resourceID gidx.PrefixedID) (ResourceRoles, error) {
|
// DeleteRole deletes the provided role.
|
||||||
path := fmt.Sprintf("/api/v1/resources/%s/roles", resourceID.String())
|
func (c *client) DeleteRole(ctx context.Context, roleID gidx.PrefixedID) error {
|
||||||
|
path := fmt.Sprintf("/api/v1/roles/%s", roleID.String())
|
||||||
|
|
||||||
var response ResourceRoles
|
var response ResourceRoleDeleteResponse
|
||||||
|
|
||||||
if _, err := c.DoRequest(ctx, http.MethodGet, path, nil, &response); err != nil {
|
if _, err := c.DoRequest(ctx, http.MethodDelete, path, nil, &response); err != nil { // nolint:bodyclose // closed by Do on json decode.
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !response.Success {
|
||||||
|
return ErrUnexpectedRoleDeleteFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListResourceRoles fetches all roles assigned to the provided resource.
|
||||||
|
func (c *client) ListResourceRoles(ctx context.Context, resourceID gidx.PrefixedID) (ResourceRoles, error) {
|
||||||
|
path := fmt.Sprintf(permsPathResourceRolesFormat, resourceID.String())
|
||||||
|
|
||||||
|
var response struct {
|
||||||
|
Data ResourceRoles `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := c.DoRequest(ctx, http.MethodGet, path, nil, &response); err != nil { // nolint:bodyclose // closed by Do on json decode.
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return response, nil
|
return response.Data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) FindResourceRoleByActions(ctx context.Context, resourceID gidx.PrefixedID, actions []string) (ResourceRole, error) {
|
// FindResourceRoleByActions fetches roles assigned to the provided resource and finds the first role where the actions match the provided actions.
|
||||||
|
func (c *client) FindResourceRoleByActions(ctx context.Context, resourceID gidx.PrefixedID, actions []string) (ResourceRole, error) {
|
||||||
roles, err := c.ListResourceRoles(ctx, resourceID)
|
roles, err := c.ListResourceRoles(ctx, resourceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ResourceRole{}, err
|
return ResourceRole{}, err
|
||||||
|
|||||||
132
internal/pubsub/nats.go
Normal file
132
internal/pubsub/nats.go
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
package pubsub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/nats-io/nats.go"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"go.infratographer.com/x/events"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
const subscriptionBufferSize = 10
|
||||||
|
|
||||||
|
type changeEvent struct {
|
||||||
|
*nats.Msg
|
||||||
|
events.ChangeMessage
|
||||||
|
|
||||||
|
Error error
|
||||||
|
}
|
||||||
|
|
||||||
|
type subscriber struct {
|
||||||
|
logger *zap.SugaredLogger
|
||||||
|
|
||||||
|
nats *nats.Conn
|
||||||
|
jetstream nats.JetStreamContext
|
||||||
|
|
||||||
|
topicPrefix string
|
||||||
|
queueGroup string
|
||||||
|
subscriptionOptions []nats.SubOpt
|
||||||
|
|
||||||
|
subscriptions []*nats.Subscription
|
||||||
|
|
||||||
|
context context.Context
|
||||||
|
cancelCtx func()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subscriber) durableName(topic string) string {
|
||||||
|
hash := md5.Sum([]byte(topic))
|
||||||
|
|
||||||
|
return s.queueGroup + hex.EncodeToString(hash[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscribeChanges subscribes to a topic, returning a channel which change events will be sent to.
|
||||||
|
func (s *subscriber) SubscribeChanges(ctx context.Context, topic string) (<-chan *changeEvent, error) {
|
||||||
|
subject := strings.Join([]string{s.topicPrefix, "changes", topic}, ".")
|
||||||
|
|
||||||
|
opts := s.subscriptionOptions
|
||||||
|
|
||||||
|
opts = append(opts, nats.Durable(s.durableName(subject)), nats.AckExplicit(), nats.ManualAck())
|
||||||
|
|
||||||
|
msgCh := make(chan *changeEvent, subscriptionBufferSize)
|
||||||
|
|
||||||
|
sub, err := s.jetstream.QueueSubscribe(subject, s.queueGroup, func(msg *nats.Msg) {
|
||||||
|
event := &changeEvent{
|
||||||
|
Msg: msg,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(bytes.NewBuffer(msg.Data)).Decode(&event.ChangeMessage); err != nil {
|
||||||
|
event.Error = err
|
||||||
|
}
|
||||||
|
|
||||||
|
msgCh <- event
|
||||||
|
}, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.subscriptions = append(s.subscriptions, sub)
|
||||||
|
|
||||||
|
go func(subscriber *nats.Subscription) {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
case <-s.context.Done():
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sub.Unsubscribe(); err != nil {
|
||||||
|
s.logger.Errorw("unable to unsubscribe", "error", err, "subject", subject)
|
||||||
|
}
|
||||||
|
}(sub)
|
||||||
|
|
||||||
|
return msgCh, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the underlying nats connection.
|
||||||
|
func (s *subscriber) Close() {
|
||||||
|
s.cancelCtx()
|
||||||
|
|
||||||
|
s.nats.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSubscriber(ctx context.Context, config events.SubscriberConfig, logger *zap.SugaredLogger, subOptions ...nats.SubOpt) (*subscriber, error) {
|
||||||
|
options := []nats.Option{
|
||||||
|
nats.Timeout(config.Timeout),
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case config.NATSConfig.CredsFile != "":
|
||||||
|
options = append(options, nats.UserCredentials(config.NATSConfig.CredsFile))
|
||||||
|
case config.NATSConfig.Token != "":
|
||||||
|
options = append(options, nats.Token(config.NATSConfig.Token))
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := nats.Connect(config.URL, options...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot connect to NATS")
|
||||||
|
}
|
||||||
|
|
||||||
|
js, err := conn.JetStream()
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
|
||||||
|
return nil, errors.Wrap(err, "cannot initialize JetStream")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
|
||||||
|
return &subscriber{
|
||||||
|
logger: logger,
|
||||||
|
nats: conn,
|
||||||
|
jetstream: js,
|
||||||
|
queueGroup: config.QueueGroup,
|
||||||
|
topicPrefix: config.Prefix,
|
||||||
|
subscriptionOptions: subOptions,
|
||||||
|
context: ctx,
|
||||||
|
cancelCtx: cancel,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -3,27 +3,30 @@ package pubsub
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
nc "github.com/nats-io/nats.go"
|
nc "github.com/nats-io/nats.go"
|
||||||
"go.infratographer.com/x/events"
|
"go.infratographer.com/x/events"
|
||||||
|
"go.infratographer.com/x/gidx"
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
"go.opentelemetry.io/otel/attribute"
|
"go.opentelemetry.io/otel/attribute"
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/models"
|
||||||
"go.equinixmetal.net/infra9-metal-bridge/internal/service"
|
"go.equinixmetal.net/infra9-metal-bridge/internal/service"
|
||||||
|
|
||||||
"github.com/ThreeDotsLabs/watermill/message"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var tracer = otel.Tracer("go.infratographer.com/permissions-api/internal/pubsub")
|
const defaultNakDelay = 10 * time.Second
|
||||||
|
|
||||||
|
var tracer = otel.Tracer("go.equinixmetal.net/infra9-metal-bridge")
|
||||||
|
|
||||||
// Subscriber is the subscriber client
|
// Subscriber is the subscriber client
|
||||||
type Subscriber struct {
|
type Subscriber struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
changeChannels []<-chan *message.Message
|
changeChannels []<-chan *changeEvent
|
||||||
logger *zap.SugaredLogger
|
logger *zap.SugaredLogger
|
||||||
subscriber *events.Subscriber
|
subscriber *subscriber
|
||||||
subOpts []nc.SubOpt
|
subOpts []nc.SubOpt
|
||||||
svc service.Service
|
svc service.Service
|
||||||
}
|
}
|
||||||
@@ -57,7 +60,7 @@ func NewSubscriber(ctx context.Context, cfg events.SubscriberConfig, service ser
|
|||||||
opt(s)
|
opt(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
sub, err := events.NewSubscriber(cfg, s.subOpts...)
|
sub, err := newSubscriber(ctx, cfg, s.logger, s.subOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -100,50 +103,60 @@ func (s Subscriber) Listen() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// listen listens for messages on a channel and calls the registered message handler
|
// listen listens for messages on a channel and calls the registered message handler
|
||||||
func (s Subscriber) listen(messages <-chan *message.Message, wg *sync.WaitGroup) {
|
func (s Subscriber) listen(messages <-chan *changeEvent, wg *sync.WaitGroup) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
for msg := range messages {
|
for msg := range messages {
|
||||||
s.logger.Infow("processing event", "event.id", msg.UUID)
|
mlogger := s.logger.With(
|
||||||
if err := s.processEvent(msg); err != nil {
|
"nats.subject", msg.Subject,
|
||||||
s.logger.Warn("Failed to process msg: ", err)
|
"event.subject.id", msg.SubjectID,
|
||||||
|
"event.type", msg.EventType,
|
||||||
|
)
|
||||||
|
|
||||||
s.logger.Infow("message nacked", "event.id", msg.UUID)
|
mlogger.Infow("processing event")
|
||||||
msg.Nack()
|
|
||||||
|
if err := s.processEvent(msg); err != nil {
|
||||||
|
mlogger.Errorw("Failed to process msg: ", "error", err)
|
||||||
|
|
||||||
|
if err = msg.NakWithDelay(defaultNakDelay); err != nil {
|
||||||
|
mlogger.Errorw("error naking failed message", "error", err)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
s.logger.Infow("message acked", "event.id", msg.UUID)
|
if err = msg.Ack(); err != nil {
|
||||||
msg.Ack()
|
mlogger.Warnw("error acking message", "error", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close closes the subscriber connection and unsubscribes from all subscriptions
|
// Close closes the subscriber connection and unsubscribes from all subscriptions
|
||||||
func (s *Subscriber) Close() error {
|
func (s *Subscriber) Close() {
|
||||||
return s.subscriber.Close()
|
s.subscriber.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// processEvent event message handler
|
// processEvent event message handler
|
||||||
func (s *Subscriber) processEvent(msg *message.Message) error {
|
func (s *Subscriber) processEvent(msg *changeEvent) error {
|
||||||
changeMsg, err := events.UnmarshalChangeMessage(msg.Payload)
|
mlogger := s.logger.With(
|
||||||
if err != nil {
|
"nats.subject", msg.Subject,
|
||||||
s.logger.Errorw("failed to process data in msg", zap.Error(err))
|
"event.subject.id", msg.SubjectID,
|
||||||
|
"event.type", msg.EventType,
|
||||||
|
)
|
||||||
|
|
||||||
return err
|
ctx, span := tracer.Start(context.Background(), "pubsub.receive", trace.WithAttributes(attribute.String("pubsub.subject", msg.SubjectID.String())))
|
||||||
}
|
|
||||||
|
|
||||||
ctx, span := tracer.Start(context.Background(), "pubsub.receive", trace.WithAttributes(attribute.String("pubsub.subject", changeMsg.SubjectID.String())))
|
|
||||||
|
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
switch events.ChangeType(changeMsg.EventType) {
|
var err error
|
||||||
|
|
||||||
|
switch events.ChangeType(msg.EventType) {
|
||||||
case events.CreateChangeType:
|
case events.CreateChangeType:
|
||||||
err = s.handleTouchEvent(ctx, msg, changeMsg)
|
err = s.handleTouchEvent(ctx, msg)
|
||||||
case events.UpdateChangeType:
|
case events.UpdateChangeType:
|
||||||
err = s.handleTouchEvent(ctx, msg, changeMsg)
|
err = s.handleTouchEvent(ctx, msg)
|
||||||
case events.DeleteChangeType:
|
case events.DeleteChangeType:
|
||||||
err = s.handleDeleteEvent(ctx, msg, changeMsg)
|
err = s.handleDeleteEvent(ctx, msg)
|
||||||
default:
|
default:
|
||||||
s.logger.Warnw("ignoring msg, not a create, update or delete event", "event_type", changeMsg.EventType)
|
mlogger.Warn("ignoring msg, not a create, update or delete event")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -153,9 +166,15 @@ func (s *Subscriber) processEvent(msg *message.Message) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Subscriber) handleTouchEvent(ctx context.Context, msg *message.Message, changeMsg events.ChangeMessage) error {
|
func (s *Subscriber) handleTouchEvent(ctx context.Context, msg *changeEvent) error {
|
||||||
if s.svc.IsOrganizationID(changeMsg.SubjectID) {
|
mlogger := s.logger.With(
|
||||||
if err := s.svc.TouchOrganization(ctx, changeMsg.SubjectID); err != nil {
|
"nats.subject", msg.Subject,
|
||||||
|
"event.subject.id", msg.SubjectID,
|
||||||
|
"event.type", msg.EventType,
|
||||||
|
)
|
||||||
|
|
||||||
|
if s.svc.IsOrganizationID(msg.SubjectID) {
|
||||||
|
if err := s.svc.TouchOrganization(ctx, msg.SubjectID); err != nil {
|
||||||
// TODO: only return errors on retryable errors
|
// TODO: only return errors on retryable errors
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -163,8 +182,8 @@ func (s *Subscriber) handleTouchEvent(ctx context.Context, msg *message.Message,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.svc.IsProjectID(changeMsg.SubjectID) {
|
if s.svc.IsProjectID(msg.SubjectID) {
|
||||||
if err := s.svc.TouchProject(ctx, changeMsg.SubjectID); err != nil {
|
if err := s.svc.TouchProject(ctx, msg.SubjectID); err != nil {
|
||||||
// TODO: only return errors on retryable errors
|
// TODO: only return errors on retryable errors
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -172,8 +191,17 @@ func (s *Subscriber) handleTouchEvent(ctx context.Context, msg *message.Message,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.svc.IsUser(changeMsg.SubjectID) {
|
if s.svc.IsUser(msg.SubjectID) {
|
||||||
if err := s.svc.TouchUser(ctx, changeMsg.SubjectID); err != nil {
|
userUUID := msg.SubjectID.String()[gidx.PrefixPartLength+1:]
|
||||||
|
|
||||||
|
subjID, err := models.GenerateSubjectID(models.IdentityPrefixUser, models.MetalUserIssuer, models.MetalUserIssuerIDPrefix+userUUID)
|
||||||
|
if err != nil {
|
||||||
|
mlogger.Errorw("failed to convert user id to identity id", "error", err)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.svc.AssignUser(ctx, subjID, msg.AdditionalSubjectIDs...); err != nil {
|
||||||
// TODO: only return errors on retryable errors
|
// TODO: only return errors on retryable errors
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -181,14 +209,20 @@ func (s *Subscriber) handleTouchEvent(ctx context.Context, msg *message.Message,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
s.logger.Warnw("unknown subject id", "subject.id", changeMsg.SubjectID)
|
mlogger.Warnw("unknown subject id")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Subscriber) handleDeleteEvent(ctx context.Context, msg *message.Message, changeMsg events.ChangeMessage) error {
|
func (s *Subscriber) handleDeleteEvent(ctx context.Context, msg *changeEvent) error {
|
||||||
if s.svc.IsOrganizationID(changeMsg.SubjectID) {
|
mlogger := s.logger.With(
|
||||||
if err := s.svc.DeleteOrganization(ctx, changeMsg.SubjectID); err != nil {
|
"nats.subject", msg.Subject,
|
||||||
|
"event.subject.id", msg.SubjectID,
|
||||||
|
"event.type", msg.EventType,
|
||||||
|
)
|
||||||
|
|
||||||
|
if s.svc.IsOrganizationID(msg.SubjectID) {
|
||||||
|
if err := s.svc.DeleteOrganization(ctx, msg.SubjectID); err != nil {
|
||||||
// TODO: only return errors on retryable errors
|
// TODO: only return errors on retryable errors
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -196,8 +230,8 @@ func (s *Subscriber) handleDeleteEvent(ctx context.Context, msg *message.Message
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.svc.IsProjectID(changeMsg.SubjectID) {
|
if s.svc.IsProjectID(msg.SubjectID) {
|
||||||
if err := s.svc.DeleteProject(ctx, changeMsg.SubjectID); err != nil {
|
if err := s.svc.DeleteProject(ctx, msg.SubjectID); err != nil {
|
||||||
// TODO: only return errors on retryable errors
|
// TODO: only return errors on retryable errors
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -205,8 +239,17 @@ func (s *Subscriber) handleDeleteEvent(ctx context.Context, msg *message.Message
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.svc.IsUser(changeMsg.SubjectID) {
|
if s.svc.IsUser(msg.SubjectID) {
|
||||||
if err := s.svc.DeleteUser(ctx, changeMsg.SubjectID); err != nil {
|
userUUID := msg.SubjectID.String()[gidx.PrefixPartLength+1:]
|
||||||
|
|
||||||
|
subjID, err := models.GenerateSubjectID(models.IdentityPrefixUser, models.MetalUserIssuer, models.MetalUserIssuerIDPrefix+userUUID)
|
||||||
|
if err != nil {
|
||||||
|
mlogger.Errorw("failed to convert user id to identity id", "error", err)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.svc.UnassignUser(ctx, subjID, msg.AdditionalSubjectIDs...); err != nil {
|
||||||
// TODO: only return errors on retryable errors
|
// TODO: only return errors on retryable errors
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -214,7 +257,7 @@ func (s *Subscriber) handleDeleteEvent(ctx context.Context, msg *message.Message
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
s.logger.Warnw("unknown subject id", "subject.id", changeMsg.SubjectID)
|
mlogger.Warnw("unknown subject id")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrInvalidJWTPrivateKeyType is returned when the private key type is not of an expected value.
|
|
||||||
ErrInvalidJWTPrivateKeyType = errors.New("invalid JWT private key provided")
|
|
||||||
|
|
||||||
// ErrAuthTokenHeaderRequired is returned when a token check request is made, but the Authorization header is missing.
|
|
||||||
ErrAuthTokenHeaderRequired = echo.NewHTTPError(http.StatusBadRequest, "header Authorization missing or invalid")
|
|
||||||
|
|
||||||
// ErrInvalidSigningMethod is returned when defined jwt signing method is not recognized.
|
|
||||||
ErrInvalidSigningMethod = errors.New("unrecognized jwt signing method provided")
|
|
||||||
|
|
||||||
// ErrMissingIssuer is returned when the jwt issuer is not defined in the config.
|
|
||||||
ErrMissingIssuer = errors.New("jwt issuer required")
|
|
||||||
)
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"go.equinixmetal.net/infra9-metal-bridge/internal/service"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Option is a functional configuration option for the router.
|
|
||||||
type Option func(r *Router) error
|
|
||||||
|
|
||||||
// WithLogger sets the logger for the router.
|
|
||||||
func WithLogger(logger *zap.Logger) Option {
|
|
||||||
return func(r *Router) error {
|
|
||||||
r.logger = logger
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithService sets the service handler.
|
|
||||||
func WithService(svc service.Service) Option {
|
|
||||||
return func(r *Router) error {
|
|
||||||
r.svc = svc
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
// Package routes provides the routes for the application.
|
|
||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"go.equinixmetal.net/infra9-metal-bridge/internal/service"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Router is the router for the application.
|
|
||||||
type Router struct {
|
|
||||||
logger *zap.Logger
|
|
||||||
svc service.Service
|
|
||||||
}
|
|
||||||
|
|
||||||
// Routes registers the routes for the application.
|
|
||||||
func (r *Router) Routes(g *echo.Group) {}
|
|
||||||
|
|
||||||
// NewRouter creates a new router
|
|
||||||
func NewRouter(opts ...Option) (*Router, error) {
|
|
||||||
router := Router{
|
|
||||||
logger: zap.NewNop(),
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, opt := range opts {
|
|
||||||
if err := opt(&router); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &router, nil
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,7 @@ package service
|
|||||||
import (
|
import (
|
||||||
"go.infratographer.com/x/gidx"
|
"go.infratographer.com/x/gidx"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
|
||||||
"go.equinixmetal.net/infra9-metal-bridge/internal/metal"
|
"go.equinixmetal.net/infra9-metal-bridge/internal/metal"
|
||||||
"go.equinixmetal.net/infra9-metal-bridge/internal/permissions"
|
"go.equinixmetal.net/infra9-metal-bridge/internal/permissions"
|
||||||
@@ -21,7 +22,7 @@ func WithLogger(logger *zap.SugaredLogger) Option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WithMetalClient sets the Equinix Metal client used by the service.
|
// WithMetalClient sets the Equinix Metal client used by the service.
|
||||||
func WithMetalClient(client *metal.Client) Option {
|
func WithMetalClient(client metal.Client) Option {
|
||||||
return func(s *service) error {
|
return func(s *service) error {
|
||||||
s.metal = client
|
s.metal = client
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ func WithMetalClient(client *metal.Client) Option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WithPermissionsClient sets the permissions client used by the service.
|
// WithPermissionsClient sets the permissions client used by the service.
|
||||||
func WithPermissionsClient(client *permissions.Client) Option {
|
func WithPermissionsClient(client permissions.Client) Option {
|
||||||
return func(s *service) error {
|
return func(s *service) error {
|
||||||
s.perms = client
|
s.perms = client
|
||||||
|
|
||||||
@@ -39,7 +40,7 @@ func WithPermissionsClient(client *permissions.Client) Option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WithPrefixMap sets the id prefix map relating id prefixes to type names.
|
// WithPrefixMap sets the id prefix map relating id prefixes to type names.
|
||||||
func WithPrefixMap(idMap map[string]string) Option {
|
func WithPrefixMap(idMap map[string]ObjectType) Option {
|
||||||
return func(s *service) error {
|
return func(s *service) error {
|
||||||
s.idPrefixMap = idMap
|
s.idPrefixMap = idMap
|
||||||
|
|
||||||
@@ -55,7 +56,7 @@ func WithRootTenant(sid string) Option {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.rootResource = rootResource{id}
|
s.rootResource = prefixedID{id}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -64,7 +65,13 @@ func WithRootTenant(sid string) Option {
|
|||||||
// WithRoles defines the role to action mapping.
|
// WithRoles defines the role to action mapping.
|
||||||
func WithRoles(roles map[string][]string) Option {
|
func WithRoles(roles map[string][]string) Option {
|
||||||
return func(s *service) error {
|
return func(s *service) error {
|
||||||
s.roles = roles
|
s.roles = make(map[string][]string, len(roles))
|
||||||
|
|
||||||
|
for role, actions := range roles {
|
||||||
|
slices.Sort(actions)
|
||||||
|
|
||||||
|
s.roles[role] = actions
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,21 +3,23 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"go.infratographer.com/x/events"
|
||||||
"go.infratographer.com/x/gidx"
|
"go.infratographer.com/x/gidx"
|
||||||
|
|
||||||
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/models"
|
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const organizationEvent = "metal_organization"
|
||||||
|
|
||||||
|
// buildOrganizationRelationships compiles all relations into a relationships object to be processed by the processors.
|
||||||
func (s *service) buildOrganizationRelationships(org *models.OrganizationDetails) (Relationships, error) {
|
func (s *service) buildOrganizationRelationships(org *models.OrganizationDetails) (Relationships, error) {
|
||||||
relations := Relationships{
|
relations := Relationships{
|
||||||
Relationships: []Relationship{
|
|
||||||
// Related org to the root tenant.
|
|
||||||
{
|
|
||||||
Resource: org,
|
Resource: org,
|
||||||
|
Parent: Relation{
|
||||||
Relation: RelateParent,
|
Relation: RelateParent,
|
||||||
RelatedResource: s.rootResource,
|
Resource: s.rootResource,
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
SubjectRelation: RelateParent,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, member := range org.Memberships {
|
for _, member := range org.Memberships {
|
||||||
@@ -29,7 +31,6 @@ func (s *service) buildOrganizationRelationships(org *models.OrganizationDetails
|
|||||||
}
|
}
|
||||||
|
|
||||||
relations.Memberships = append(relations.Memberships, ResourceMemberships{
|
relations.Memberships = append(relations.Memberships, ResourceMemberships{
|
||||||
Resource: org,
|
|
||||||
Role: role,
|
Role: role,
|
||||||
Member: member.User,
|
Member: member.User,
|
||||||
})
|
})
|
||||||
@@ -37,32 +38,16 @@ func (s *service) buildOrganizationRelationships(org *models.OrganizationDetails
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, project := range org.Projects {
|
for _, project := range org.Projects {
|
||||||
relations.Relationships = append(relations.Relationships, Relationship{
|
relations.SubjectRelationships = append(relations.SubjectRelationships, Relation{
|
||||||
Resource: project,
|
Resource: project,
|
||||||
Relation: RelateParent,
|
Relation: RelateParent,
|
||||||
RelatedResource: org,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
for _, member := range project.Memberships {
|
|
||||||
for _, role := range member.Roles {
|
|
||||||
if _, ok := s.roles[role]; !ok {
|
|
||||||
s.logger.Warnf("unrecognized project role '%s' for %s on %s", role, member.User.PrefixedID(), project.PrefixedID())
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
relations.Memberships = append(relations.Memberships, ResourceMemberships{
|
|
||||||
Resource: project,
|
|
||||||
Role: role,
|
|
||||||
Member: member.User,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return relations, nil
|
return relations, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsOrganizationID checks if the provided id has the metal organization prefix.
|
||||||
func (s *service) IsOrganizationID(id gidx.PrefixedID) bool {
|
func (s *service) IsOrganizationID(id gidx.PrefixedID) bool {
|
||||||
if idType, ok := s.idPrefixMap[id.Prefix()]; ok {
|
if idType, ok := s.idPrefixMap[id.Prefix()]; ok {
|
||||||
return idType == TypeOrganization
|
return idType == TypeOrganization
|
||||||
@@ -71,6 +56,7 @@ func (s *service) IsOrganizationID(id gidx.PrefixedID) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TouchOrganization initializes a sync for the provided organization id for relationships and memberships.
|
||||||
func (s *service) TouchOrganization(ctx context.Context, id gidx.PrefixedID) error {
|
func (s *service) TouchOrganization(ctx context.Context, id gidx.PrefixedID) error {
|
||||||
logger := s.logger.With("organization.id", id.String())
|
logger := s.logger.With("organization.id", id.String())
|
||||||
|
|
||||||
@@ -88,14 +74,32 @@ func (s *service) TouchOrganization(ctx context.Context, id gidx.PrefixedID) err
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.processRelationships(ctx, "metal-relation", relationships.Relationships)
|
relationshipChanges := s.processRelationships(ctx, organizationEvent, relationships)
|
||||||
s.processMemberships(ctx, relationships.Memberships)
|
rolesChanged, assignmentsChanged := s.processMemberships(ctx, relationships, false)
|
||||||
|
|
||||||
s.logger.Infow("organization sync complete", "relationships", len(relationships.Relationships), "memberships", len(relationships.Memberships))
|
s.logger.Infow("organization sync complete",
|
||||||
|
"resource.id", org.PrefixedID(),
|
||||||
|
"relationships.changed", relationshipChanges,
|
||||||
|
"membership.roles_changed", rolesChanged,
|
||||||
|
"membership.assignments_changed", assignmentsChanged,
|
||||||
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteOrganization deletes the provided organization id.
|
||||||
func (s *service) DeleteOrganization(ctx context.Context, id gidx.PrefixedID) error {
|
func (s *service) DeleteOrganization(ctx context.Context, id gidx.PrefixedID) error {
|
||||||
|
err := s.publisher.PublishChange(ctx, organizationEvent, events.ChangeMessage{
|
||||||
|
SubjectID: id,
|
||||||
|
EventType: string(events.DeleteChangeType),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Errorw("error publishing organization delete",
|
||||||
|
"subject_type", organizationEvent,
|
||||||
|
"resource.id", id,
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
357
internal/service/organizations_test.go
Normal file
357
internal/service/organizations_test.go
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
package service_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.infratographer.com/x/events"
|
||||||
|
"go.infratographer.com/x/gidx"
|
||||||
|
|
||||||
|
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/models"
|
||||||
|
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers"
|
||||||
|
"go.equinixmetal.net/infra9-metal-bridge/internal/permissions"
|
||||||
|
"go.equinixmetal.net/infra9-metal-bridge/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockPublisher struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *mockPublisher) PublishChange(ctx context.Context, subjectType string, change events.ChangeMessage) error {
|
||||||
|
args := p.Called(subjectType, change)
|
||||||
|
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTouchOrganizationEmpty(t *testing.T) {
|
||||||
|
rootTenantID := gidx.PrefixedID("tnntten-root1")
|
||||||
|
|
||||||
|
roleMap := map[string][]string{
|
||||||
|
"owner": {
|
||||||
|
"action1",
|
||||||
|
"action2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
userID := gidx.PrefixedID("idntusr-2aPXqbgfYSlld36XLfvNjEqUIIL9ekRZzgs2eBGOCBw")
|
||||||
|
user := &models.UserDetails{
|
||||||
|
ID: "usr1",
|
||||||
|
}
|
||||||
|
|
||||||
|
projectID := gidx.PrefixedID("metlprj-prj1")
|
||||||
|
project := &models.ProjectDetails{
|
||||||
|
ID: "prj1",
|
||||||
|
}
|
||||||
|
|
||||||
|
orgID := gidx.PrefixedID("metlorg-org1")
|
||||||
|
org := &models.OrganizationDetails{
|
||||||
|
ID: "org1",
|
||||||
|
Projects: []*models.ProjectDetails{
|
||||||
|
project,
|
||||||
|
},
|
||||||
|
Memberships: []*models.Membership[models.OrganizationDetails]{
|
||||||
|
{
|
||||||
|
User: user,
|
||||||
|
Roles: []string{
|
||||||
|
"owner",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
mMetal = new(providers.MockProvider)
|
||||||
|
mPerms = new(permissions.MockClient)
|
||||||
|
mPublisher = new(mockPublisher)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Relationships
|
||||||
|
|
||||||
|
mMetal.On("GetOrganizationDetails", orgID).Return(org, nil)
|
||||||
|
|
||||||
|
mPerms.On("ListResourceRelationshipsFrom", orgID).Return([]permissions.ResourceRelationship{}, nil)
|
||||||
|
mPerms.On("ListResourceRelationshipsTo", orgID).Return([]permissions.ResourceRelationship{}, nil)
|
||||||
|
|
||||||
|
orgTenantChangeMessage := events.ChangeMessage{
|
||||||
|
SubjectID: orgID,
|
||||||
|
EventType: string(events.CreateChangeType),
|
||||||
|
AdditionalSubjectIDs: []gidx.PrefixedID{
|
||||||
|
rootTenantID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
relateProjectChangeMessage := events.ChangeMessage{
|
||||||
|
SubjectID: projectID,
|
||||||
|
EventType: string(events.CreateChangeType),
|
||||||
|
AdditionalSubjectIDs: []gidx.PrefixedID{
|
||||||
|
orgID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mPublisher.On("PublishChange", "metal_organization", orgTenantChangeMessage).Return(nil)
|
||||||
|
mPublisher.On("PublishChange", "metal_organization", relateProjectChangeMessage).Return(nil)
|
||||||
|
|
||||||
|
// Memberships
|
||||||
|
|
||||||
|
newRoleID := gidx.PrefixedID("permrol-role1")
|
||||||
|
|
||||||
|
mPerms.On("ListResourceRoles", orgID).Return(permissions.ResourceRoles{}, nil)
|
||||||
|
mPerms.On("CreateRole", orgID, roleMap["owner"]).Return(newRoleID, nil)
|
||||||
|
|
||||||
|
mPerms.On("AssignRole", newRoleID, userID).Return(nil)
|
||||||
|
|
||||||
|
// Run scenario
|
||||||
|
|
||||||
|
svc, err := service.New(mPublisher, mMetal, mPerms,
|
||||||
|
service.WithRootTenant(rootTenantID.String()),
|
||||||
|
service.WithRoles(roleMap),
|
||||||
|
)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = svc.TouchOrganization(context.Background(), orgID)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.True(t, mMetal.AssertExpectations(t), "unexpected calls to metal provider")
|
||||||
|
require.True(t, mPerms.AssertExpectations(t), "unexpected calls to permissions client")
|
||||||
|
require.True(t, mPublisher.AssertExpectations(t), "unexpected calls to events publisher")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTouchOrganizationCleanup(t *testing.T) {
|
||||||
|
oldRootTenantID := gidx.PrefixedID("tnntten-root1")
|
||||||
|
newRootTenantID := gidx.PrefixedID("tnntten-root2")
|
||||||
|
|
||||||
|
roleMap := map[string][]string{
|
||||||
|
"owner": {
|
||||||
|
"action1",
|
||||||
|
"action2",
|
||||||
|
},
|
||||||
|
"collaborator": {
|
||||||
|
"action1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
oldUserID := gidx.PrefixedID("idntusr-2aPXqbgfYSlld36XLfvNjEqUIIL9ekRZzgs2eBGOCBw")
|
||||||
|
deadProjectID := gidx.PrefixedID("metlprj-prj1")
|
||||||
|
|
||||||
|
userID := gidx.PrefixedID("idntusr-Ewj_PJUue9eDIDoyCoWG48GtwlysqTj2Y4qWPiJPN1s")
|
||||||
|
user := &models.UserDetails{
|
||||||
|
ID: "usr2",
|
||||||
|
}
|
||||||
|
|
||||||
|
projectID := gidx.PrefixedID("metlprj-prj2")
|
||||||
|
project := &models.ProjectDetails{
|
||||||
|
ID: "prj2",
|
||||||
|
}
|
||||||
|
|
||||||
|
orgID := gidx.PrefixedID("metlorg-org1")
|
||||||
|
org := &models.OrganizationDetails{
|
||||||
|
ID: "org1",
|
||||||
|
Projects: []*models.ProjectDetails{
|
||||||
|
project,
|
||||||
|
},
|
||||||
|
Memberships: []*models.Membership[models.OrganizationDetails]{
|
||||||
|
{
|
||||||
|
User: user,
|
||||||
|
Roles: []string{
|
||||||
|
"owner",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
mMetal = new(providers.MockProvider)
|
||||||
|
mPerms = new(permissions.MockClient)
|
||||||
|
mPublisher = new(mockPublisher)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Relationships
|
||||||
|
|
||||||
|
mMetal.On("GetOrganizationDetails", orgID).Return(org, nil)
|
||||||
|
|
||||||
|
existingRelsFrom := []permissions.ResourceRelationship{
|
||||||
|
{
|
||||||
|
Relation: string(service.RelateParent),
|
||||||
|
SubjectID: oldRootTenantID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mPerms.On("ListResourceRelationshipsFrom", orgID).Return(existingRelsFrom, nil)
|
||||||
|
|
||||||
|
existingRelsTo := []permissions.ResourceRelationship{
|
||||||
|
{
|
||||||
|
ResourceID: deadProjectID,
|
||||||
|
Relation: string(service.RelateParent),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mPerms.On("ListResourceRelationshipsTo", orgID).Return(existingRelsTo, nil)
|
||||||
|
|
||||||
|
mPerms.On("DeleteResourceRelationship", orgID, string(service.RelateParent), oldRootTenantID).Return(nil)
|
||||||
|
mPerms.On("DeleteResourceRelationship", deadProjectID, string(service.RelateParent), orgID).Return(nil)
|
||||||
|
|
||||||
|
orgTenantChangeMessage := events.ChangeMessage{
|
||||||
|
SubjectID: orgID,
|
||||||
|
EventType: string(events.CreateChangeType),
|
||||||
|
AdditionalSubjectIDs: []gidx.PrefixedID{
|
||||||
|
newRootTenantID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
relateProjectChangeMessage := events.ChangeMessage{
|
||||||
|
SubjectID: projectID,
|
||||||
|
EventType: string(events.CreateChangeType),
|
||||||
|
AdditionalSubjectIDs: []gidx.PrefixedID{
|
||||||
|
orgID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mPublisher.On("PublishChange", "metal_organization", orgTenantChangeMessage).Return(nil)
|
||||||
|
mPublisher.On("PublishChange", "metal_organization", relateProjectChangeMessage).Return(nil)
|
||||||
|
|
||||||
|
// Memberships
|
||||||
|
|
||||||
|
oldRoleID := gidx.PrefixedID("permrol-role1")
|
||||||
|
newRoleID := gidx.PrefixedID("permrol-role2")
|
||||||
|
|
||||||
|
existingRoles := permissions.ResourceRoles{
|
||||||
|
{
|
||||||
|
ID: oldRoleID,
|
||||||
|
Actions: roleMap["collaborator"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mPerms.On("ListResourceRoles", orgID).Return(existingRoles, nil)
|
||||||
|
|
||||||
|
existingRoleAssignments := []gidx.PrefixedID{
|
||||||
|
oldUserID,
|
||||||
|
}
|
||||||
|
|
||||||
|
mPerms.On("ListRoleAssignments", oldRoleID).Return(existingRoleAssignments, nil)
|
||||||
|
|
||||||
|
mPerms.On("CreateRole", orgID, roleMap["owner"]).Return(newRoleID, nil)
|
||||||
|
mPerms.On("DeleteRole", oldRoleID).Return(nil)
|
||||||
|
|
||||||
|
mPerms.On("AssignRole", newRoleID, userID).Return(nil)
|
||||||
|
mPerms.On("UnassignRole", oldRoleID, oldUserID).Return(nil)
|
||||||
|
|
||||||
|
// Run scenario
|
||||||
|
|
||||||
|
svc, err := service.New(mPublisher, mMetal, mPerms,
|
||||||
|
service.WithRootTenant(newRootTenantID.String()),
|
||||||
|
service.WithRoles(roleMap),
|
||||||
|
)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = svc.TouchOrganization(context.Background(), orgID)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.True(t, mMetal.AssertExpectations(t), "unexpected calls to metal provider")
|
||||||
|
require.True(t, mPerms.AssertExpectations(t), "unexpected calls to permissions client")
|
||||||
|
require.True(t, mPublisher.AssertExpectations(t), "unexpected calls to events publisher")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTouchOrganizationNoChange(t *testing.T) {
|
||||||
|
rootTenantID := gidx.PrefixedID("tnntten-root1")
|
||||||
|
|
||||||
|
roleMap := map[string][]string{
|
||||||
|
"owner": {
|
||||||
|
"action1",
|
||||||
|
"action2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
userID := gidx.PrefixedID("idntusr-2aPXqbgfYSlld36XLfvNjEqUIIL9ekRZzgs2eBGOCBw")
|
||||||
|
user := &models.UserDetails{
|
||||||
|
ID: "usr1",
|
||||||
|
}
|
||||||
|
|
||||||
|
projectID := gidx.PrefixedID("metlprj-prj1")
|
||||||
|
project := &models.ProjectDetails{
|
||||||
|
ID: "prj1",
|
||||||
|
}
|
||||||
|
|
||||||
|
orgID := gidx.PrefixedID("metlorg-org1")
|
||||||
|
org := &models.OrganizationDetails{
|
||||||
|
ID: "org1",
|
||||||
|
Projects: []*models.ProjectDetails{
|
||||||
|
project,
|
||||||
|
},
|
||||||
|
Memberships: []*models.Membership[models.OrganizationDetails]{
|
||||||
|
{
|
||||||
|
User: user,
|
||||||
|
Roles: []string{
|
||||||
|
"owner",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
mMetal = new(providers.MockProvider)
|
||||||
|
mPerms = new(permissions.MockClient)
|
||||||
|
mPublisher = new(mockPublisher)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Relationships
|
||||||
|
|
||||||
|
mMetal.On("GetOrganizationDetails", orgID).Return(org, nil)
|
||||||
|
|
||||||
|
existingRelsFrom := []permissions.ResourceRelationship{
|
||||||
|
{
|
||||||
|
Relation: string(service.RelateParent),
|
||||||
|
SubjectID: rootTenantID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mPerms.On("ListResourceRelationshipsFrom", orgID).Return(existingRelsFrom, nil)
|
||||||
|
|
||||||
|
existingRelsTo := []permissions.ResourceRelationship{
|
||||||
|
{
|
||||||
|
ResourceID: projectID,
|
||||||
|
Relation: string(service.RelateParent),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mPerms.On("ListResourceRelationshipsTo", orgID).Return(existingRelsTo, nil)
|
||||||
|
|
||||||
|
// Memberships
|
||||||
|
|
||||||
|
roleID := gidx.PrefixedID("permrol-role1")
|
||||||
|
|
||||||
|
existingRoles := permissions.ResourceRoles{
|
||||||
|
{
|
||||||
|
ID: roleID,
|
||||||
|
Actions: roleMap["owner"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mPerms.On("ListResourceRoles", orgID).Return(existingRoles, nil)
|
||||||
|
|
||||||
|
existingRoleAssignments := []gidx.PrefixedID{
|
||||||
|
userID,
|
||||||
|
}
|
||||||
|
|
||||||
|
mPerms.On("ListRoleAssignments", roleID).Return(existingRoleAssignments, nil)
|
||||||
|
|
||||||
|
// Run scenario
|
||||||
|
|
||||||
|
svc, err := service.New(mPublisher, mMetal, mPerms,
|
||||||
|
service.WithRootTenant(rootTenantID.String()),
|
||||||
|
service.WithRoles(roleMap),
|
||||||
|
)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = svc.TouchOrganization(context.Background(), orgID)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.True(t, mMetal.AssertExpectations(t), "unexpected calls to metal provider")
|
||||||
|
require.True(t, mPerms.AssertExpectations(t), "unexpected calls to permissions client")
|
||||||
|
require.True(t, mPublisher.AssertExpectations(t), "unexpected calls to events publisher")
|
||||||
|
}
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"go.infratographer.com/x/events"
|
|
||||||
"go.infratographer.com/x/gidx"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *service) processRelationships(ctx context.Context, subjectType string, relationships []Relationship) {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
for _, rel := range relationships {
|
|
||||||
err = s.publisher.PublishChange(ctx, subjectType, events.ChangeMessage{
|
|
||||||
SubjectID: rel.Resource.PrefixedID(),
|
|
||||||
EventType: string(events.CreateChangeType),
|
|
||||||
AdditionalSubjectIDs: []gidx.PrefixedID{
|
|
||||||
rel.RelatedResource.PrefixedID(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
s.logger.Errorw("error publishing change",
|
|
||||||
"subject_type", subjectType,
|
|
||||||
"resource.id", rel.Resource.PrefixedID(),
|
|
||||||
"related_resource.id", rel.RelatedResource.PrefixedID(),
|
|
||||||
"error", err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *service) processMemberships(ctx context.Context, memberships []ResourceMemberships) {
|
|
||||||
resourceRoleID := make(map[gidx.PrefixedID]map[string]gidx.PrefixedID)
|
|
||||||
resourceRoleMembers := make(map[gidx.PrefixedID]map[string]map[gidx.PrefixedID]bool)
|
|
||||||
roleActions := make(map[string][]string)
|
|
||||||
|
|
||||||
for _, membership := range memberships {
|
|
||||||
resourceID := membership.Resource.PrefixedID()
|
|
||||||
role := membership.Role
|
|
||||||
memberID := membership.Member.PrefixedID()
|
|
||||||
|
|
||||||
if _, ok := resourceRoleMembers[resourceID]; !ok {
|
|
||||||
resourceRoleID[resourceID] = make(map[string]gidx.PrefixedID)
|
|
||||||
resourceRoleMembers[resourceID] = make(map[string]map[gidx.PrefixedID]bool)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := resourceRoleMembers[resourceID][role]; !ok {
|
|
||||||
resourceRoleMembers[resourceID][role] = make(map[gidx.PrefixedID]bool)
|
|
||||||
roleActions[role] = s.roles[role]
|
|
||||||
}
|
|
||||||
|
|
||||||
resourceRoleMembers[resourceID][role][memberID] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
resourceRoleAssignments := make(map[gidx.PrefixedID]map[gidx.PrefixedID]map[gidx.PrefixedID]bool)
|
|
||||||
|
|
||||||
for resourceID, roles := range resourceRoleID {
|
|
||||||
resourceRoleAssignments[resourceID] = make(map[gidx.PrefixedID]map[gidx.PrefixedID]bool)
|
|
||||||
|
|
||||||
for role := range roles {
|
|
||||||
actions := roleActions[role]
|
|
||||||
|
|
||||||
resourceRole, err := s.perms.FindResourceRoleByActions(ctx, resourceID, actions)
|
|
||||||
if err != nil {
|
|
||||||
s.logger.Warnw("failed to find role by actions for resource", "resource.id", resourceID, "role", role, "actions", actions, "error", err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
resourceRoleID[resourceID][role] = resourceRole.ID
|
|
||||||
resourceRoleAssignments[resourceID][resourceRole.ID] = make(map[gidx.PrefixedID]bool)
|
|
||||||
|
|
||||||
assignments, err := s.perms.ListRoleAssignments(ctx, resourceRole.ID)
|
|
||||||
if err != nil {
|
|
||||||
s.logger.Warnw("failed to get role assignments for resource", "resource.id", resourceID, "role", role, "error", err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, assignment := range assignments {
|
|
||||||
resourceRoleAssignments[resourceID][resourceRole.ID][assignment] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for resourceID, roles := range resourceRoleMembers {
|
|
||||||
for role, members := range roles {
|
|
||||||
roleID := resourceRoleID[resourceID][role]
|
|
||||||
actions := roleActions[role]
|
|
||||||
|
|
||||||
logger := s.logger.With("resource.id", resourceID, "role.name", role, "actions", actions)
|
|
||||||
|
|
||||||
var createdRole bool
|
|
||||||
|
|
||||||
if roleID == gidx.NullPrefixedID {
|
|
||||||
logger.Infow("creating role for resource")
|
|
||||||
|
|
||||||
resourceRoleID, err := s.perms.CreateRole(ctx, resourceID, actions)
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorw("failed to create role for resource", "error", err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
createdRole = true
|
|
||||||
roleID = resourceRoleID
|
|
||||||
}
|
|
||||||
|
|
||||||
logger = logger.With("role.id", roleID)
|
|
||||||
|
|
||||||
assignments := make(map[gidx.PrefixedID]bool)
|
|
||||||
|
|
||||||
if !createdRole {
|
|
||||||
assignments = resourceRoleAssignments[resourceID][roleID]
|
|
||||||
}
|
|
||||||
|
|
||||||
for memberID := range members {
|
|
||||||
mlogger := logger.With("member.id", memberID)
|
|
||||||
|
|
||||||
if _, ok := assignments[memberID]; ok {
|
|
||||||
mlogger.Infow("skipping already assigned member")
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.perms.AssignRole(ctx, roleID, memberID); err != nil {
|
|
||||||
mlogger.Errorw("failed to assign member to role", "error", err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
mlogger.Infow("role assigned to member")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
235
internal/service/process_memberships.go
Normal file
235
internal/service/process_memberships.go
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go.infratographer.com/x/gidx"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
|
||||||
|
"go.equinixmetal.net/infra9-metal-bridge/internal/permissions"
|
||||||
|
)
|
||||||
|
|
||||||
|
// processMemberships determines the changes between what is wanted and what is live and executes on the differences.
|
||||||
|
// If skipDeletions is true, no deletes will be executed.
|
||||||
|
func (s *service) processMemberships(ctx context.Context, relationships Relationships, skipDeletions bool) (int, int) {
|
||||||
|
if len(relationships.Memberships) == 0 {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
rlogger := s.logger.With("resource.id", relationships.Resource.PrefixedID())
|
||||||
|
|
||||||
|
roleIDs := make(map[string]gidx.PrefixedID)
|
||||||
|
|
||||||
|
var (
|
||||||
|
totalRoleCreate, totalRoleDelete int
|
||||||
|
totalRoleAssign, totalRoleUnassign int
|
||||||
|
)
|
||||||
|
|
||||||
|
wantRoles, wantAssignments := s.mapResourceWants(relationships.Memberships)
|
||||||
|
|
||||||
|
liveRoles, liveAssignments, err := s.mapResourceDetails(ctx, relationships.Resource.PrefixedID())
|
||||||
|
if err != nil {
|
||||||
|
rlogger.Errorw("failed to get membership resource details map",
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
roleCreations := make(map[string][]string)
|
||||||
|
roleDeletions := make([]gidx.PrefixedID, 0)
|
||||||
|
|
||||||
|
for roleKey, actions := range wantRoles {
|
||||||
|
if _, ok := liveRoles[roleKey]; !ok {
|
||||||
|
roleCreations[roleKey] = actions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for roleKey, role := range liveRoles {
|
||||||
|
if !skipDeletions {
|
||||||
|
if _, ok := wantRoles[roleKey]; !ok {
|
||||||
|
roleDeletions = append(roleDeletions, role.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
roleIDs[roleKey] = role.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
roleMembershipsAdd := make(map[string][]gidx.PrefixedID)
|
||||||
|
roleMembershipsRemove := make(map[string][]gidx.PrefixedID)
|
||||||
|
|
||||||
|
for roleKey, assignments := range wantAssignments {
|
||||||
|
for memberID := range assignments {
|
||||||
|
if _, ok := liveAssignments[roleKey]; ok {
|
||||||
|
if _, ok := liveAssignments[roleKey][memberID]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
roleMembershipsAdd[roleKey] = append(roleMembershipsAdd[roleKey], memberID)
|
||||||
|
totalRoleAssign++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !skipDeletions {
|
||||||
|
for roleKey, assignments := range liveAssignments {
|
||||||
|
for memberID := range assignments {
|
||||||
|
if _, ok := wantAssignments[roleKey]; ok {
|
||||||
|
if _, ok := wantAssignments[roleKey][memberID]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
roleMembershipsRemove[roleKey] = append(roleMembershipsRemove[roleKey], memberID)
|
||||||
|
totalRoleUnassign++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
totalRoleCreate += len(roleCreations)
|
||||||
|
totalRoleDelete += len(roleDeletions)
|
||||||
|
|
||||||
|
rlogger.Debugw("processing memberships",
|
||||||
|
"role.create", totalRoleCreate,
|
||||||
|
"role.delete", totalRoleDelete,
|
||||||
|
"role.assign", totalRoleAssign,
|
||||||
|
"role.unassign", totalRoleUnassign,
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
rolesCreated, rolesDeleted int
|
||||||
|
roleAssignments, roleUnassignments int
|
||||||
|
)
|
||||||
|
|
||||||
|
for roleKey, actions := range roleCreations {
|
||||||
|
roleID, err := s.perms.CreateRole(ctx, relationships.Resource.PrefixedID(), actions)
|
||||||
|
if err != nil {
|
||||||
|
rlogger.Errorw("error creating role", "actions", actions, "error", err)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
roleIDs[roleKey] = roleID
|
||||||
|
rolesCreated++
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, roleID := range roleDeletions {
|
||||||
|
if err := s.perms.DeleteRole(ctx, roleID); err != nil {
|
||||||
|
rlogger.Errorw("error deleting role", "role.id", roleID, "error", err)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rolesDeleted++
|
||||||
|
}
|
||||||
|
|
||||||
|
for roleKey, members := range roleMembershipsAdd {
|
||||||
|
roleID, ok := roleIDs[roleKey]
|
||||||
|
if !ok {
|
||||||
|
rlogger.Errorw("role id not found for role actions key", "role_actions_key", roleKey, "members", len(members))
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, memberID := range members {
|
||||||
|
if err := s.perms.AssignRole(ctx, roleID, memberID); err != nil {
|
||||||
|
rlogger.Errorw("error assigning member to role", "role.id", roleID, "member.id", memberID, "error", err)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
roleAssignments++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for roleKey, members := range roleMembershipsRemove {
|
||||||
|
roleID, ok := roleIDs[roleKey]
|
||||||
|
if !ok {
|
||||||
|
rlogger.Errorw("role id not found for role actions key", "role_actions_key", roleKey, "members", len(members))
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, memberID := range members {
|
||||||
|
if err := s.perms.UnassignRole(ctx, roleID, memberID); err != nil {
|
||||||
|
rlogger.Errorw("error removing member from role", "role.id", roleID, "member.id", memberID, "error", err)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
roleUnassignments++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rlogger.Debugw("memberships processed",
|
||||||
|
"role.create", rolesCreated,
|
||||||
|
"role.delete", rolesDeleted,
|
||||||
|
"role.assign", roleAssignments,
|
||||||
|
"role.unassign", roleUnassignments,
|
||||||
|
)
|
||||||
|
|
||||||
|
return rolesCreated + rolesDeleted, roleAssignments + roleUnassignments
|
||||||
|
}
|
||||||
|
|
||||||
|
// mapResourceWants processes the provided memberships and returns two maps.
|
||||||
|
// A Role Key is computed based on a sorted slice of actions for each role.
|
||||||
|
// The first map is of Role Key -> list of actions
|
||||||
|
// The second map is of Role Key -> Member ID -> true
|
||||||
|
func (s *service) mapResourceWants(memberships []ResourceMemberships) (map[string][]string, map[string]map[gidx.PrefixedID]bool) {
|
||||||
|
roleActionsKey := make(map[string]string)
|
||||||
|
|
||||||
|
for role, actions := range s.roles {
|
||||||
|
roleActionsKey[role] = strings.Join(actions, "|")
|
||||||
|
}
|
||||||
|
|
||||||
|
wantRoles := make(map[string][]string)
|
||||||
|
wantAssignments := make(map[string]map[gidx.PrefixedID]bool)
|
||||||
|
|
||||||
|
for _, membership := range memberships {
|
||||||
|
roleKey := roleActionsKey[membership.Role]
|
||||||
|
|
||||||
|
if _, ok := wantRoles[roleKey]; !ok {
|
||||||
|
wantRoles[roleKey] = s.roles[membership.Role]
|
||||||
|
wantAssignments[roleKey] = make(map[gidx.PrefixedID]bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantAssignments[roleKey][membership.Member.PrefixedID()] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return wantRoles, wantAssignments
|
||||||
|
}
|
||||||
|
|
||||||
|
// mapResourceDetails fetches the provided ResourceID's live state and returns two maps and an error.
|
||||||
|
// A Role Key is computed based on a sorted slice of actions for each role.
|
||||||
|
// The first map is of Role Key -> Permissions Resource Role
|
||||||
|
// The second map is of Role Key -> Member ID -> true
|
||||||
|
func (s *service) mapResourceDetails(ctx context.Context, resourceID gidx.PrefixedID) (map[string]permissions.ResourceRole, map[string]map[gidx.PrefixedID]bool, error) {
|
||||||
|
roles := make(map[string]permissions.ResourceRole)
|
||||||
|
assignments := make(map[string]map[gidx.PrefixedID]bool)
|
||||||
|
|
||||||
|
liveRoles, err := s.perms.ListResourceRoles(ctx, resourceID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, role := range liveRoles {
|
||||||
|
slices.Sort(role.Actions)
|
||||||
|
|
||||||
|
roleKey := strings.Join(role.Actions, "|")
|
||||||
|
roles[roleKey] = role
|
||||||
|
|
||||||
|
liveAssignments, err := s.perms.ListRoleAssignments(ctx, role.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
assignments[roleKey] = make(map[gidx.PrefixedID]bool)
|
||||||
|
|
||||||
|
for _, assignment := range liveAssignments {
|
||||||
|
assignments[roleKey][assignment] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return roles, assignments, nil
|
||||||
|
}
|
||||||
216
internal/service/process_relationships.go
Normal file
216
internal/service/process_relationships.go
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go.infratographer.com/x/events"
|
||||||
|
"go.infratographer.com/x/gidx"
|
||||||
|
|
||||||
|
"go.equinixmetal.net/infra9-metal-bridge/internal/permissions"
|
||||||
|
)
|
||||||
|
|
||||||
|
// processRelationships determines the changes between what is wanted and what is live and executes on the differences.
|
||||||
|
// Relationship creations use events.
|
||||||
|
// Relationship deletions use the api, as delete events delete all related resources and not just the provided ones.
|
||||||
|
func (s *service) processRelationships(ctx context.Context, eventType string, relationships Relationships) int {
|
||||||
|
rlogger := s.logger.With("resource.id", relationships.Resource.PrefixedID())
|
||||||
|
|
||||||
|
wantParentRelationship, wantSubjectRelationships := s.mapRelationWants(relationships)
|
||||||
|
|
||||||
|
liveParentRelationships, liveSubjectRelationships, err := s.getRelationshipMap(ctx, relationships.Resource, relationships.SubjectRelation)
|
||||||
|
if err != nil {
|
||||||
|
rlogger.Errorw("failed to get relationship map",
|
||||||
|
"relationships.subject_relation", relationships.SubjectRelation,
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
createParentRelationship *Relation
|
||||||
|
deleteParentRelationships []gidx.PrefixedID
|
||||||
|
|
||||||
|
foundParent bool
|
||||||
|
|
||||||
|
createSubjectRelationships []Relation
|
||||||
|
deleteSubjectRelationships []Relation
|
||||||
|
)
|
||||||
|
|
||||||
|
if wantParentRelationship != nil {
|
||||||
|
for subjID := range liveParentRelationships {
|
||||||
|
if subjID == wantParentRelationship.Resource.PrefixedID() {
|
||||||
|
foundParent = true
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteParentRelationships = append(deleteParentRelationships, subjID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundParent {
|
||||||
|
createParentRelationship = wantParentRelationship
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for subjID := range liveParentRelationships {
|
||||||
|
deleteParentRelationships = append(deleteParentRelationships, subjID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for resID, relation := range wantSubjectRelationships {
|
||||||
|
if _, ok := liveSubjectRelationships[resID]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
createSubjectRelationships = append(createSubjectRelationships, Relation{
|
||||||
|
Resource: prefixedID{resID},
|
||||||
|
Relation: relation,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for resID, relation := range liveSubjectRelationships {
|
||||||
|
if _, ok := wantSubjectRelationships[resID]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteSubjectRelationships = append(deleteSubjectRelationships, Relation{
|
||||||
|
Resource: prefixedID{resID},
|
||||||
|
Relation: relation,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var processEvents []events.ChangeMessage
|
||||||
|
|
||||||
|
rlogger.Debugw("processing relationships",
|
||||||
|
"parent.create", createParentRelationship != nil,
|
||||||
|
"parent.delete", len(deleteParentRelationships),
|
||||||
|
"subject.create", len(createSubjectRelationships),
|
||||||
|
"subject.delete", len(deleteSubjectRelationships),
|
||||||
|
)
|
||||||
|
|
||||||
|
if createParentRelationship != nil {
|
||||||
|
processEvents = append(processEvents, events.ChangeMessage{
|
||||||
|
SubjectID: relationships.Resource.PrefixedID(),
|
||||||
|
EventType: string(events.CreateChangeType),
|
||||||
|
AdditionalSubjectIDs: []gidx.PrefixedID{
|
||||||
|
createParentRelationship.Resource.PrefixedID(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, relation := range createSubjectRelationships {
|
||||||
|
processEvents = append(processEvents, events.ChangeMessage{
|
||||||
|
SubjectID: relation.Resource.PrefixedID(),
|
||||||
|
EventType: string(events.CreateChangeType),
|
||||||
|
AdditionalSubjectIDs: []gidx.PrefixedID{
|
||||||
|
relationships.Resource.PrefixedID(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, relatedResourceID := range deleteParentRelationships {
|
||||||
|
err = s.perms.DeleteResourceRelationship(ctx, relationships.Resource.PrefixedID(), string(RelateParent), relatedResourceID)
|
||||||
|
if err != nil {
|
||||||
|
rlogger.Errorw("error deleting parent relationship",
|
||||||
|
"parent.resource.id", relatedResourceID.String(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, relation := range deleteSubjectRelationships {
|
||||||
|
err = s.perms.DeleteResourceRelationship(ctx, relation.Resource.PrefixedID(), string(relation.Relation), relationships.Resource.PrefixedID())
|
||||||
|
if err != nil {
|
||||||
|
rlogger.Errorw("error deleting relationship",
|
||||||
|
"relation", relation.Relation,
|
||||||
|
"subject.id", relation.Resource.PrefixedID().String(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, event := range processEvents {
|
||||||
|
err = s.publisher.PublishChange(ctx, eventType, event)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rlogger.Errorw("error publishing change",
|
||||||
|
"subject_type", eventType,
|
||||||
|
"subject.id", event.SubjectID,
|
||||||
|
"event.type", event.EventType,
|
||||||
|
"additional_subject_ids", event.AdditionalSubjectIDs,
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rlogger.Debugw("relationships processed",
|
||||||
|
"parent.create", createParentRelationship != nil,
|
||||||
|
"parent.delete", len(deleteParentRelationships),
|
||||||
|
"subject.create", len(createSubjectRelationships),
|
||||||
|
"subject.delete", len(deleteSubjectRelationships),
|
||||||
|
)
|
||||||
|
|
||||||
|
changes := len(deleteParentRelationships) + len(createSubjectRelationships) + len(deleteSubjectRelationships)
|
||||||
|
|
||||||
|
if createParentRelationship != nil {
|
||||||
|
changes++
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes
|
||||||
|
}
|
||||||
|
|
||||||
|
// mapRelationWants returns the parent relation if provided and a map of Subjects -> relation.
|
||||||
|
func (s *service) mapRelationWants(relationships Relationships) (*Relation, map[gidx.PrefixedID]RelationshipType) {
|
||||||
|
var wantParent *Relation
|
||||||
|
|
||||||
|
wantSubject := make(map[gidx.PrefixedID]RelationshipType)
|
||||||
|
|
||||||
|
if relationships.Parent.Resource != nil {
|
||||||
|
wantParent = &relationships.Parent
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, relationship := range relationships.SubjectRelationships {
|
||||||
|
wantSubject[relationship.Resource.PrefixedID()] = relationship.Relation
|
||||||
|
}
|
||||||
|
|
||||||
|
return wantParent, wantSubject
|
||||||
|
}
|
||||||
|
|
||||||
|
// getRelationshipMap fetches the provided resources relationships, as the source resource and the destination subject.
|
||||||
|
// Returned are two maps, the first maps Subject IDs -> Relationship
|
||||||
|
// The second map, maps Resource IDs -> relationship
|
||||||
|
func (s *service) getRelationshipMap(ctx context.Context, resource IDPrefixableResource, relation RelationshipType) (map[gidx.PrefixedID]RelationshipType, map[gidx.PrefixedID]RelationshipType, error) {
|
||||||
|
liveResource, err := s.perms.ListResourceRelationshipsFrom(ctx, resource.PrefixedID())
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var liveSubject []permissions.ResourceRelationship
|
||||||
|
|
||||||
|
if relation != "" {
|
||||||
|
liveSubject, err = s.perms.ListResourceRelationshipsTo(ctx, resource.PrefixedID())
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parents := make(map[gidx.PrefixedID]RelationshipType, len(liveResource))
|
||||||
|
|
||||||
|
for _, relationship := range liveResource {
|
||||||
|
if relationship.Relation != string(RelateParent) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parents[relationship.SubjectID] = RelationshipType(relationship.Relation)
|
||||||
|
}
|
||||||
|
|
||||||
|
subject := make(map[gidx.PrefixedID]RelationshipType, len(liveSubject))
|
||||||
|
|
||||||
|
for _, relationship := range liveSubject {
|
||||||
|
if relationship.Relation != string(relation) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
subject[relationship.ResourceID] = RelationshipType(relationship.Relation)
|
||||||
|
}
|
||||||
|
|
||||||
|
return parents, subject, nil
|
||||||
|
}
|
||||||
@@ -3,9 +3,44 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"go.infratographer.com/x/events"
|
||||||
"go.infratographer.com/x/gidx"
|
"go.infratographer.com/x/gidx"
|
||||||
|
|
||||||
|
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const projectEvent = "metal_project"
|
||||||
|
|
||||||
|
// buildProjectRelationships compiles all relations into a relationships object to be processed by the processors.
|
||||||
|
func (s *service) buildProjectRelationships(project *models.ProjectDetails) (Relationships, error) {
|
||||||
|
relations := Relationships{
|
||||||
|
Resource: project,
|
||||||
|
// Relate project to organization.
|
||||||
|
Parent: Relation{
|
||||||
|
Resource: project.Organization,
|
||||||
|
Relation: RelateParent,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, member := range project.Memberships {
|
||||||
|
for _, role := range member.Roles {
|
||||||
|
if _, ok := s.roles[role]; !ok {
|
||||||
|
s.logger.Warnf("unrecognized project role '%s' for %s on %s", role, member.User.PrefixedID(), project.PrefixedID())
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
relations.Memberships = append(relations.Memberships, ResourceMemberships{
|
||||||
|
Role: role,
|
||||||
|
Member: member.User,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return relations, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsProjectID checks if the provided id has the metal project prefix.
|
||||||
func (s *service) IsProjectID(id gidx.PrefixedID) bool {
|
func (s *service) IsProjectID(id gidx.PrefixedID) bool {
|
||||||
if idType, ok := s.idPrefixMap[id.Prefix()]; ok {
|
if idType, ok := s.idPrefixMap[id.Prefix()]; ok {
|
||||||
return idType == TypeProject
|
return idType == TypeProject
|
||||||
@@ -14,10 +49,50 @@ func (s *service) IsProjectID(id gidx.PrefixedID) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TouchProject initializes a sync for the provided project id for relationships and memberships.
|
||||||
func (s *service) TouchProject(ctx context.Context, id gidx.PrefixedID) error {
|
func (s *service) TouchProject(ctx context.Context, id gidx.PrefixedID) error {
|
||||||
|
logger := s.logger.With("project.id", id.String())
|
||||||
|
|
||||||
|
project, err := s.metal.GetProjectDetails(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorw("failed to get project", "error", err)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
relationships, err := s.buildProjectRelationships(project)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorw("failed to build project relationships", "error", err)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
relationshipChanges := s.processRelationships(ctx, projectEvent, relationships)
|
||||||
|
rolesChanged, assignmentsChanged := s.processMemberships(ctx, relationships, false)
|
||||||
|
|
||||||
|
s.logger.Infow("project sync complete",
|
||||||
|
"resource.id", project.PrefixedID(),
|
||||||
|
"relationships.changed", relationshipChanges,
|
||||||
|
"membership.roles_changed", rolesChanged,
|
||||||
|
"membership.assignments_changed", assignmentsChanged,
|
||||||
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteProject deletes the provided project id.
|
||||||
func (s *service) DeleteProject(ctx context.Context, id gidx.PrefixedID) error {
|
func (s *service) DeleteProject(ctx context.Context, id gidx.PrefixedID) error {
|
||||||
|
err := s.publisher.PublishChange(ctx, projectEvent, events.ChangeMessage{
|
||||||
|
SubjectID: id,
|
||||||
|
EventType: string(events.DeleteChangeType),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Errorw("error publishing project delete",
|
||||||
|
"subject_type", projectEvent,
|
||||||
|
"resource.id", id,
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
303
internal/service/projects_test.go
Normal file
303
internal/service/projects_test.go
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
package service_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.infratographer.com/x/events"
|
||||||
|
"go.infratographer.com/x/gidx"
|
||||||
|
|
||||||
|
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/models"
|
||||||
|
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers"
|
||||||
|
"go.equinixmetal.net/infra9-metal-bridge/internal/permissions"
|
||||||
|
"go.equinixmetal.net/infra9-metal-bridge/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTouchProjectEmpty(t *testing.T) {
|
||||||
|
rootTenantID := gidx.PrefixedID("tnntten-root1")
|
||||||
|
|
||||||
|
roleMap := map[string][]string{
|
||||||
|
"owner": {
|
||||||
|
"action1",
|
||||||
|
"action2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
userID := gidx.PrefixedID("idntusr-2aPXqbgfYSlld36XLfvNjEqUIIL9ekRZzgs2eBGOCBw")
|
||||||
|
user := &models.UserDetails{
|
||||||
|
ID: "usr1",
|
||||||
|
}
|
||||||
|
|
||||||
|
orgID := gidx.PrefixedID("metlorg-org1")
|
||||||
|
org := &models.OrganizationDetails{
|
||||||
|
ID: "org1",
|
||||||
|
}
|
||||||
|
|
||||||
|
projectID := gidx.PrefixedID("metlprj-prj1")
|
||||||
|
project := &models.ProjectDetails{
|
||||||
|
ID: "prj1",
|
||||||
|
Organization: org,
|
||||||
|
Memberships: []*models.Membership[models.ProjectDetails]{
|
||||||
|
{
|
||||||
|
User: user,
|
||||||
|
Roles: []string{
|
||||||
|
"owner",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
mMetal = new(providers.MockProvider)
|
||||||
|
mPerms = new(permissions.MockClient)
|
||||||
|
mPublisher = new(mockPublisher)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Relationships
|
||||||
|
|
||||||
|
mMetal.On("GetProjectDetails", projectID).Return(project, nil)
|
||||||
|
|
||||||
|
mPerms.On("ListResourceRelationshipsFrom", projectID).Return([]permissions.ResourceRelationship{}, nil)
|
||||||
|
|
||||||
|
relateParentChangeMessage := events.ChangeMessage{
|
||||||
|
SubjectID: projectID,
|
||||||
|
EventType: string(events.CreateChangeType),
|
||||||
|
AdditionalSubjectIDs: []gidx.PrefixedID{
|
||||||
|
orgID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mPublisher.On("PublishChange", "metal_project", relateParentChangeMessage).Return(nil)
|
||||||
|
|
||||||
|
// Memberships
|
||||||
|
|
||||||
|
newRoleID := gidx.PrefixedID("permrol-role1")
|
||||||
|
|
||||||
|
mPerms.On("ListResourceRoles", projectID).Return(permissions.ResourceRoles{}, nil)
|
||||||
|
mPerms.On("CreateRole", projectID, roleMap["owner"]).Return(newRoleID, nil)
|
||||||
|
|
||||||
|
mPerms.On("AssignRole", newRoleID, userID).Return(nil)
|
||||||
|
|
||||||
|
// Run scenario
|
||||||
|
|
||||||
|
svc, err := service.New(mPublisher, mMetal, mPerms,
|
||||||
|
service.WithRootTenant(rootTenantID.String()),
|
||||||
|
service.WithRoles(roleMap),
|
||||||
|
)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = svc.TouchProject(context.Background(), projectID)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.True(t, mMetal.AssertExpectations(t), "unexpected calls to metal provider")
|
||||||
|
require.True(t, mPerms.AssertExpectations(t), "unexpected calls to permissions client")
|
||||||
|
require.True(t, mPublisher.AssertExpectations(t), "unexpected calls to events publisher")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTouchProjectCleanup(t *testing.T) {
|
||||||
|
rootTenantID := gidx.PrefixedID("tnntten-root1")
|
||||||
|
|
||||||
|
roleMap := map[string][]string{
|
||||||
|
"owner": {
|
||||||
|
"action1",
|
||||||
|
"action2",
|
||||||
|
},
|
||||||
|
"collaborator": {
|
||||||
|
"action1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
oldUserID := gidx.PrefixedID("idntusr-2aPXqbgfYSlld36XLfvNjEqUIIL9ekRZzgs2eBGOCBw")
|
||||||
|
oldOrgID := gidx.PrefixedID("metlorg-org1")
|
||||||
|
|
||||||
|
userID := gidx.PrefixedID("idntusr-Ewj_PJUue9eDIDoyCoWG48GtwlysqTj2Y4qWPiJPN1s")
|
||||||
|
user := &models.UserDetails{
|
||||||
|
ID: "usr2",
|
||||||
|
}
|
||||||
|
|
||||||
|
orgID := gidx.PrefixedID("metlorg-org2")
|
||||||
|
org := &models.OrganizationDetails{
|
||||||
|
ID: "org2",
|
||||||
|
}
|
||||||
|
|
||||||
|
projectID := gidx.PrefixedID("metlprj-prj2")
|
||||||
|
project := &models.ProjectDetails{
|
||||||
|
ID: "prj2",
|
||||||
|
Organization: org,
|
||||||
|
Memberships: []*models.Membership[models.ProjectDetails]{
|
||||||
|
{
|
||||||
|
User: user,
|
||||||
|
Roles: []string{
|
||||||
|
"owner",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
mMetal = new(providers.MockProvider)
|
||||||
|
mPerms = new(permissions.MockClient)
|
||||||
|
mPublisher = new(mockPublisher)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Relationships
|
||||||
|
|
||||||
|
mMetal.On("GetProjectDetails", projectID).Return(project, nil)
|
||||||
|
|
||||||
|
existingRelsFrom := []permissions.ResourceRelationship{
|
||||||
|
{
|
||||||
|
Relation: string(service.RelateParent),
|
||||||
|
SubjectID: oldOrgID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mPerms.On("ListResourceRelationshipsFrom", projectID).Return(existingRelsFrom, nil)
|
||||||
|
|
||||||
|
mPerms.On("DeleteResourceRelationship", projectID, string(service.RelateParent), oldOrgID).Return(nil)
|
||||||
|
|
||||||
|
relateParentChangeMessage := events.ChangeMessage{
|
||||||
|
SubjectID: projectID,
|
||||||
|
EventType: string(events.CreateChangeType),
|
||||||
|
AdditionalSubjectIDs: []gidx.PrefixedID{
|
||||||
|
orgID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mPublisher.On("PublishChange", "metal_project", relateParentChangeMessage).Return(nil)
|
||||||
|
|
||||||
|
// Memberships
|
||||||
|
|
||||||
|
oldRoleID := gidx.PrefixedID("permrol-role1")
|
||||||
|
newRoleID := gidx.PrefixedID("permrol-role2")
|
||||||
|
|
||||||
|
existingRoles := permissions.ResourceRoles{
|
||||||
|
{
|
||||||
|
ID: oldRoleID,
|
||||||
|
Actions: roleMap["collaborator"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mPerms.On("ListResourceRoles", projectID).Return(existingRoles, nil)
|
||||||
|
|
||||||
|
existingRoleAssignments := []gidx.PrefixedID{
|
||||||
|
oldUserID,
|
||||||
|
}
|
||||||
|
|
||||||
|
mPerms.On("ListRoleAssignments", oldRoleID).Return(existingRoleAssignments, nil)
|
||||||
|
|
||||||
|
mPerms.On("CreateRole", projectID, roleMap["owner"]).Return(newRoleID, nil)
|
||||||
|
mPerms.On("DeleteRole", oldRoleID).Return(nil)
|
||||||
|
|
||||||
|
mPerms.On("AssignRole", newRoleID, userID).Return(nil)
|
||||||
|
mPerms.On("UnassignRole", oldRoleID, oldUserID).Return(nil)
|
||||||
|
|
||||||
|
// Run scenario
|
||||||
|
|
||||||
|
svc, err := service.New(mPublisher, mMetal, mPerms,
|
||||||
|
service.WithRootTenant(rootTenantID.String()),
|
||||||
|
service.WithRoles(roleMap),
|
||||||
|
)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = svc.TouchProject(context.Background(), projectID)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.True(t, mMetal.AssertExpectations(t), "unexpected calls to metal provider")
|
||||||
|
require.True(t, mPerms.AssertExpectations(t), "unexpected calls to permissions client")
|
||||||
|
require.True(t, mPublisher.AssertExpectations(t), "unexpected calls to events publisher")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTouchProjectNoChange(t *testing.T) {
|
||||||
|
rootTenantID := gidx.PrefixedID("tnntten-root1")
|
||||||
|
|
||||||
|
roleMap := map[string][]string{
|
||||||
|
"owner": {
|
||||||
|
"action1",
|
||||||
|
"action2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
userID := gidx.PrefixedID("idntusr-2aPXqbgfYSlld36XLfvNjEqUIIL9ekRZzgs2eBGOCBw")
|
||||||
|
user := &models.UserDetails{
|
||||||
|
ID: "usr1",
|
||||||
|
}
|
||||||
|
|
||||||
|
orgID := gidx.PrefixedID("metlorg-org1")
|
||||||
|
org := &models.OrganizationDetails{
|
||||||
|
ID: "org1",
|
||||||
|
}
|
||||||
|
|
||||||
|
projectID := gidx.PrefixedID("metlprj-prj1")
|
||||||
|
project := &models.ProjectDetails{
|
||||||
|
ID: "prj1",
|
||||||
|
Organization: org,
|
||||||
|
Memberships: []*models.Membership[models.ProjectDetails]{
|
||||||
|
{
|
||||||
|
User: user,
|
||||||
|
Roles: []string{
|
||||||
|
"owner",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
mMetal = new(providers.MockProvider)
|
||||||
|
mPerms = new(permissions.MockClient)
|
||||||
|
mPublisher = new(mockPublisher)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Relationships
|
||||||
|
|
||||||
|
mMetal.On("GetProjectDetails", projectID).Return(project, nil)
|
||||||
|
|
||||||
|
existingRelsFrom := []permissions.ResourceRelationship{
|
||||||
|
{
|
||||||
|
Relation: string(service.RelateParent),
|
||||||
|
SubjectID: orgID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mPerms.On("ListResourceRelationshipsFrom", projectID).Return(existingRelsFrom, nil)
|
||||||
|
|
||||||
|
// Memberships
|
||||||
|
|
||||||
|
roleID := gidx.PrefixedID("permrol-role1")
|
||||||
|
|
||||||
|
existingRoles := permissions.ResourceRoles{
|
||||||
|
{
|
||||||
|
ID: roleID,
|
||||||
|
Actions: roleMap["owner"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mPerms.On("ListResourceRoles", projectID).Return(existingRoles, nil)
|
||||||
|
|
||||||
|
existingRoleAssignments := []gidx.PrefixedID{
|
||||||
|
userID,
|
||||||
|
}
|
||||||
|
|
||||||
|
mPerms.On("ListRoleAssignments", roleID).Return(existingRoleAssignments, nil)
|
||||||
|
|
||||||
|
// Run scenario
|
||||||
|
|
||||||
|
svc, err := service.New(mPublisher, mMetal, mPerms,
|
||||||
|
service.WithRootTenant(rootTenantID.String()),
|
||||||
|
service.WithRoles(roleMap),
|
||||||
|
)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = svc.TouchProject(context.Background(), projectID)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.True(t, mMetal.AssertExpectations(t), "unexpected calls to metal provider")
|
||||||
|
require.True(t, mPerms.AssertExpectations(t), "unexpected calls to permissions client")
|
||||||
|
require.True(t, mPublisher.AssertExpectations(t), "unexpected calls to events publisher")
|
||||||
|
}
|
||||||
@@ -2,63 +2,41 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"go.infratographer.com/x/gidx"
|
"go.infratographer.com/x/gidx"
|
||||||
|
|
||||||
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/models"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// RelateOwner is the owner relationship type.
|
||||||
RelateOwner RelationshipType = "owner"
|
RelateOwner RelationshipType = "owner"
|
||||||
|
|
||||||
|
// RelateParent is the parent relationship type.
|
||||||
RelateParent RelationshipType = "parent"
|
RelateParent RelationshipType = "parent"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// RelationshipType are relationship types.
|
||||||
type RelationshipType string
|
type RelationshipType string
|
||||||
|
|
||||||
|
// IDPrefixableResource ensures the the interface passed provides prefixed ids.
|
||||||
type IDPrefixableResource interface {
|
type IDPrefixableResource interface {
|
||||||
PrefixedID() gidx.PrefixedID
|
PrefixedID() gidx.PrefixedID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Relationships defines a resource and all possible relationships and memberships.
|
||||||
type Relationships struct {
|
type Relationships struct {
|
||||||
Relationships []Relationship
|
Resource IDPrefixableResource
|
||||||
|
Parent Relation
|
||||||
|
SubjectRelation RelationshipType
|
||||||
|
SubjectRelationships []Relation
|
||||||
Memberships []ResourceMemberships
|
Memberships []ResourceMemberships
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r Relationships) DeDupe() Relationships {
|
// Relation defines a relation to a resource.
|
||||||
rels := make(map[string]bool)
|
type Relation struct {
|
||||||
mems := make(map[string]bool)
|
|
||||||
|
|
||||||
var results Relationships
|
|
||||||
|
|
||||||
for _, rel := range r.Relationships {
|
|
||||||
key := rel.Resource.PrefixedID().String() + "/" + string(rel.Relation) + "/" + rel.RelatedResource.PrefixedID().String()
|
|
||||||
|
|
||||||
if _, ok := rels[key]; !ok {
|
|
||||||
rels[key] = true
|
|
||||||
|
|
||||||
results.Relationships = append(results.Relationships, rel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, member := range r.Memberships {
|
|
||||||
key := member.Resource.PrefixedID().String() + "/" + member.Role + "/" + member.Member.PrefixedID().String()
|
|
||||||
|
|
||||||
if _, ok := mems[key]; !ok {
|
|
||||||
mems[key] = true
|
|
||||||
|
|
||||||
results.Memberships = append(results.Memberships, member)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
type Relationship struct {
|
|
||||||
Resource IDPrefixableResource
|
|
||||||
Relation RelationshipType
|
Relation RelationshipType
|
||||||
RelatedResource IDPrefixableResource
|
Resource IDPrefixableResource
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResourceMemberships defines a member and role.
|
||||||
type ResourceMemberships struct {
|
type ResourceMemberships struct {
|
||||||
Resource IDPrefixableResource
|
|
||||||
Role string
|
Role string
|
||||||
Member *models.UserDetails
|
Member IDPrefixableResource
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,20 +13,42 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// TypeOrganization defines the organization type.
|
// TypeOrganization defines the organization type.
|
||||||
TypeOrganization = "organization"
|
TypeOrganization ObjectType = "organization"
|
||||||
|
|
||||||
// TypeProject defines the project type.
|
// TypeProject defines the project type.
|
||||||
TypeProject = "project"
|
TypeProject ObjectType = "project"
|
||||||
|
|
||||||
// TypeUser defines the user type.
|
// TypeUser defines the user type.
|
||||||
TypeUser = "user"
|
TypeUser ObjectType = "user"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultPrefixMap is the default id prefix to type relationship.
|
// DefaultPrefixMap is the default id prefix to type relationship.
|
||||||
var DefaultPrefixMap = map[string]string{
|
var DefaultPrefixMap = map[string]ObjectType{
|
||||||
"metlorg": TypeOrganization,
|
TypeOrganization.Prefix(): TypeOrganization,
|
||||||
"metlprj": TypeProject,
|
TypeProject.Prefix(): TypeProject,
|
||||||
"metlusr": TypeUser,
|
TypeUser.Prefix(): TypeUser,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObjectType defines a type of object.
|
||||||
|
type ObjectType string
|
||||||
|
|
||||||
|
// Prefix returns the objects id prefix.
|
||||||
|
func (t ObjectType) Prefix() string {
|
||||||
|
switch t {
|
||||||
|
case TypeOrganization:
|
||||||
|
return "metlorg"
|
||||||
|
case TypeProject:
|
||||||
|
return "metlprj"
|
||||||
|
case TypeUser:
|
||||||
|
return "metlusr"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string fo the object type.
|
||||||
|
func (t ObjectType) String() string {
|
||||||
|
return string(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Service defines a bridge service methods
|
// Service defines a bridge service methods
|
||||||
@@ -47,39 +69,48 @@ type Service interface {
|
|||||||
|
|
||||||
// IsUser checks if the provided id has an id prefix which is a user.
|
// IsUser checks if the provided id has an id prefix which is a user.
|
||||||
IsUser(id gidx.PrefixedID) bool
|
IsUser(id gidx.PrefixedID) bool
|
||||||
// TouchUser triggers a sync of a user and their permissions.
|
// AssignUser assigns a user to the given resource.
|
||||||
TouchUser(ctx context.Context, id gidx.PrefixedID) error
|
AssignUser(ctx context.Context, userID gidx.PrefixedID, resourceIDs ...gidx.PrefixedID) error
|
||||||
// DeleteUser deletes the user and their permissions.
|
// UnassignUser removes the users from the given resource.
|
||||||
DeleteUser(ctx context.Context, id gidx.PrefixedID) error
|
UnassignUser(ctx context.Context, userID gidx.PrefixedID, resourceIDs ...gidx.PrefixedID) error
|
||||||
|
|
||||||
|
// IsAssignableResource checks if the provided resource ID may have assigned users.
|
||||||
|
IsAssignableResource(id gidx.PrefixedID) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// EventPublisher defines the required methods to publish events.
|
||||||
|
type EventPublisher interface {
|
||||||
|
PublishChange(ctx context.Context, subjectType string, change events.ChangeMessage) error
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Service = &service{}
|
var _ Service = &service{}
|
||||||
|
|
||||||
type service struct {
|
type service struct {
|
||||||
logger *zap.SugaredLogger
|
logger *zap.SugaredLogger
|
||||||
publisher *events.Publisher
|
publisher EventPublisher
|
||||||
metal *metal.Client
|
metal metal.Client
|
||||||
perms *permissions.Client
|
perms permissions.Client
|
||||||
idPrefixMap map[string]string
|
idPrefixMap map[string]ObjectType
|
||||||
|
|
||||||
rootResource rootResource
|
rootResource prefixedID
|
||||||
roles map[string][]string
|
roles map[string][]string
|
||||||
}
|
}
|
||||||
|
|
||||||
type rootResource struct {
|
type prefixedID struct {
|
||||||
id gidx.PrefixedID
|
id gidx.PrefixedID
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r rootResource) PrefixedID() gidx.PrefixedID {
|
func (r prefixedID) PrefixedID() gidx.PrefixedID {
|
||||||
return r.id
|
return r.id
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(publisher *events.Publisher, metal *metal.Client, perms *permissions.Client, options ...Option) (Service, error) {
|
// New creates a new service.
|
||||||
|
func New(publisher EventPublisher, metal metal.Client, perms permissions.Client, options ...Option) (Service, error) {
|
||||||
svc := &service{
|
svc := &service{
|
||||||
publisher: publisher,
|
publisher: publisher,
|
||||||
metal: metal,
|
metal: metal,
|
||||||
perms: perms,
|
perms: perms,
|
||||||
idPrefixMap: make(map[string]string),
|
idPrefixMap: make(map[string]ObjectType),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, opt := range options {
|
for _, opt := range options {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"go.infratographer.com/x/gidx"
|
"go.infratographer.com/x/gidx"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// IsUser checks the provided id has the metal user prefix.
|
||||||
func (s *service) IsUser(id gidx.PrefixedID) bool {
|
func (s *service) IsUser(id gidx.PrefixedID) bool {
|
||||||
if idType, ok := s.idPrefixMap[id.Prefix()]; ok {
|
if idType, ok := s.idPrefixMap[id.Prefix()]; ok {
|
||||||
return idType == TypeUser
|
return idType == TypeUser
|
||||||
@@ -14,10 +15,134 @@ func (s *service) IsUser(id gidx.PrefixedID) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) TouchUser(ctx context.Context, id gidx.PrefixedID) error {
|
// IsAssignableResource checks that the provided id is an id which can have memberships assignments.
|
||||||
|
func (s *service) IsAssignableResource(id gidx.PrefixedID) bool {
|
||||||
|
if idType, ok := s.idPrefixMap[id.Prefix()]; ok {
|
||||||
|
switch idType {
|
||||||
|
case TypeOrganization, TypeProject:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assignuser assigns the provided users to the given resource ids.
|
||||||
|
func (s *service) AssignUser(ctx context.Context, userID gidx.PrefixedID, resourceIDs ...gidx.PrefixedID) error {
|
||||||
|
var totalResources, rolesChanged, assignmentsChanged int
|
||||||
|
|
||||||
|
mlogger := s.logger.With("member.id", userID.String())
|
||||||
|
|
||||||
|
memberID := prefixedID{userID}
|
||||||
|
|
||||||
|
for _, resourceID := range resourceIDs {
|
||||||
|
role, err := s.getUserResourceRole(ctx, userID, resourceID)
|
||||||
|
if err != nil {
|
||||||
|
mlogger.Warnw("failed to determine role for user resource", "error", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if role == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
roles, assignments := s.processMemberships(ctx, Relationships{
|
||||||
|
Resource: prefixedID{resourceID},
|
||||||
|
Memberships: []ResourceMemberships{
|
||||||
|
{
|
||||||
|
Role: role,
|
||||||
|
Member: memberID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, true)
|
||||||
|
|
||||||
|
totalResources++
|
||||||
|
|
||||||
|
rolesChanged += roles
|
||||||
|
assignmentsChanged += assignments
|
||||||
|
}
|
||||||
|
|
||||||
|
mlogger.Infow("assignment sync complete",
|
||||||
|
"membership.roles_changed", rolesChanged,
|
||||||
|
"membership.assignments_changed", assignmentsChanged,
|
||||||
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) DeleteUser(ctx context.Context, id gidx.PrefixedID) error {
|
// UnassignUser removes the assignment for the provided user id to the given resources.
|
||||||
|
func (s *service) UnassignUser(ctx context.Context, userID gidx.PrefixedID, resourceIDs ...gidx.PrefixedID) error {
|
||||||
|
for _, resourceID := range resourceIDs {
|
||||||
|
rlogger := s.logger.With("user.id", userID, "resource.id", resourceID)
|
||||||
|
|
||||||
|
role, err := s.getUserResourceRole(ctx, userID, resourceID)
|
||||||
|
if err != nil {
|
||||||
|
rlogger.Warnw("failed to determine role for user resource", "error", err)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if role == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
actions := s.roles[role]
|
||||||
|
|
||||||
|
rlogger = rlogger.With("role.name", role, "role.actions", actions)
|
||||||
|
|
||||||
|
resourceRole, err := s.perms.FindResourceRoleByActions(ctx, resourceID, actions)
|
||||||
|
if err != nil {
|
||||||
|
rlogger.Warnw("failed to find role by actions for resource", "error", err)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rlogger = rlogger.With("role.id", resourceRole.ID)
|
||||||
|
|
||||||
|
assigned, err := s.perms.RoleHasAssignment(ctx, resourceRole.ID, userID)
|
||||||
|
if err != nil {
|
||||||
|
rlogger.Warnw("failed to check role assignment", "error", err)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !assigned {
|
||||||
|
rlogger.Warnw("unable to unassign member which is not assigned")
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = s.perms.UnassignRole(ctx, resourceRole.ID, userID); err != nil {
|
||||||
|
rlogger.Errorw("failed to unassign member from role", "error", err)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getuserResourceRole fetches the appropriate object types user role for the given resource.
|
||||||
|
func (s *service) getUserResourceRole(ctx context.Context, userID, resourceID gidx.PrefixedID) (string, error) {
|
||||||
|
var (
|
||||||
|
role string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if idType, ok := s.idPrefixMap[resourceID.Prefix()]; ok {
|
||||||
|
switch idType {
|
||||||
|
case TypeOrganization:
|
||||||
|
role, err = s.metal.GetUserOrganizationRole(ctx, userID, resourceID)
|
||||||
|
case TypeProject:
|
||||||
|
role, err = s.metal.GetUserProjectRole(ctx, userID, resourceID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return role, nil
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user