mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 19:21:50 +00:00
msp: generate projectID up-front and persist in spec (#59220)
This is a big diff, but they all tie together, so hear me out: The only way to get the project ID right now is to query the appropriate Terraform Cloud workspace outputs. However, to do that, you need access to `sourcegraph-secrets`, to get the appropriate TFC access token. This is awkward because as an operator, you would follow the instructions to request `mspServiceEditor` on your desired project - but now, to use various MSP tooling like `sg msp pg connect`, you must _also_ request access to `sourcegraph-secrets`, so that we can get a TFC token to find the project ID and other stuff. Because we might have a large number of services it's not feasible to manually set up Entitle bundles (they cannot be programmatically created). The approach I want to take is to copy the MSP team TFC token from `sourcegraph-secrets` into each individual MSP environment project. Then, we can get the MSP team TFC token from the _environment_ project instead, access for which will be granted by the `mspServiceEditor` role. To do this however, we must know the project ID up front. So this PR makes the following changes: 1. Makes it so that the randomized project ID isn't managed by Terraform, but generated statically, and configured in `environments[].projectID`. 2. This requires changes to `sg msp init` to create a project ID the same way we create it in-Terraform today, but in addition to service initialization, we must now also have tooling to start configuration a new environment as well, so that we can generate a project ID for the operator. This is done via a new command, `sg msp init-env`, which inserts a new environment into a service spec. - MSP service specs are intended to be operator-written and hopefully include lots of docstrings on configuration, so we take special care to preserve formatting and comments by manipulating `yaml.Node` directly. 4. In order to use `yaml.Node`, however, we must switch over to `gopkg.in/yaml.v3` - previously, we used the K8S YAML library, mostly as a carry-over from what is used in Cloud. In order to use `gopkg.in/yaml.v3`, we need to: - Replace all `json` struct tags with `yaml`, as the YAML library does not support JSON tags - Upgrade `github.com/invopop/jsonschema` so that we can point the JSON schema generator to use the `yaml` tags as well 5. Now that we have `projectID` statically available, we can remove code that queries TFC workspaces for the project ID and replace them with references to the spec instead. ## Test plan 1. Unit tests on `sg msp init`'s generated output 2. Unit tests on inserting environment 3. Unit tests on project ID generator 4. https://github.com/sourcegraph/managed-services/pull/295
This commit is contained in:
parent
09536d25d3
commit
4a640b5e96
15
deps.bzl
15
deps.bzl
@ -3406,13 +3406,6 @@ def go_dependencies():
|
||||
sum = "h1:JR7eDj8HD6eXrc5fWLbSUnfcQFL06PYvCc0DKQnWfaU=",
|
||||
version = "v1.0.0",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_iancoleman_orderedmap",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/iancoleman/orderedmap",
|
||||
sum = "h1:sq1N/TFpYH++aViPcaKjys3bDClUEU7s5B+z6jq8pNA=",
|
||||
version = "v0.2.0",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_iancoleman_strcase",
|
||||
build_file_proto_mode = "disable_global",
|
||||
@ -3459,8 +3452,8 @@ def go_dependencies():
|
||||
name = "com_github_invopop_jsonschema",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/invopop/jsonschema",
|
||||
sum = "h1:2vgQcBz1n256N+FpX3Jq7Y17AjYt46Ig3zIWyy770So=",
|
||||
version = "v0.7.0",
|
||||
sum = "h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI=",
|
||||
version = "v0.12.0",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_ionos_cloud_sdk_go_v6",
|
||||
@ -6151,8 +6144,8 @@ def go_dependencies():
|
||||
name = "com_github_wk8_go_ordered_map_v2",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/wk8/go-ordered-map/v2",
|
||||
sum = "h1:jLbYIFyWQMUwHLO20cImlCRBoNc5lp0nmE2dvwcxc7k=",
|
||||
version = "v2.1.5",
|
||||
sum = "h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=",
|
||||
version = "v2.1.8",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_wsxiaoys_terminal",
|
||||
|
||||
@ -6,13 +6,11 @@ go_library(
|
||||
importpath = "github.com/sourcegraph/sourcegraph/dev/managedservicesplatform/internal/stack/project",
|
||||
visibility = ["//dev/managedservicesplatform:__subpackages__"],
|
||||
deps = [
|
||||
"//dev/managedservicesplatform/internal/resource/random",
|
||||
"//dev/managedservicesplatform/internal/resourceid",
|
||||
"//dev/managedservicesplatform/internal/stack",
|
||||
"//dev/managedservicesplatform/internal/stack/options/googleprovider",
|
||||
"//dev/managedservicesplatform/internal/stack/options/randomprovider",
|
||||
"//dev/managedservicesplatform/spec",
|
||||
"//lib/errors",
|
||||
"//lib/pointers",
|
||||
"@com_github_aws_jsii_runtime_go//:jsii-runtime-go",
|
||||
"@com_github_grafana_regexp//:regexp",
|
||||
|
||||
@ -9,13 +9,11 @@ import (
|
||||
"github.com/sourcegraph/managed-services-platform-cdktf/gen/google/project"
|
||||
"github.com/sourcegraph/managed-services-platform-cdktf/gen/google/projectservice"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/dev/managedservicesplatform/internal/resource/random"
|
||||
"github.com/sourcegraph/sourcegraph/dev/managedservicesplatform/internal/resourceid"
|
||||
"github.com/sourcegraph/sourcegraph/dev/managedservicesplatform/internal/stack"
|
||||
"github.com/sourcegraph/sourcegraph/dev/managedservicesplatform/internal/stack/options/googleprovider"
|
||||
"github.com/sourcegraph/sourcegraph/dev/managedservicesplatform/internal/stack/options/randomprovider"
|
||||
"github.com/sourcegraph/sourcegraph/dev/managedservicesplatform/spec"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
"github.com/sourcegraph/sourcegraph/lib/pointers"
|
||||
)
|
||||
|
||||
@ -65,13 +63,9 @@ type CrossStackOutput struct {
|
||||
}
|
||||
|
||||
type Variables struct {
|
||||
// ProjectIDPrefix is the prefix for a project ID. A suffix of the format
|
||||
// '-${randomizedSuffix}' will be added, as project IDs must be unique.
|
||||
ProjectIDPrefix string
|
||||
|
||||
// ProjectIDSuffixLength is the length of the randomized suffix added to
|
||||
// to the project.
|
||||
ProjectIDSuffixLength *int
|
||||
// ProjectIDPrefix is the generated project ID. A suffix of the format
|
||||
// '-${randomizedSuffix}' should be added, as project IDs must be unique.
|
||||
ProjectID string
|
||||
|
||||
// DisplayName is a display name for the project. It does not need to be unique.
|
||||
DisplayName string
|
||||
@ -92,46 +86,25 @@ type Variables struct {
|
||||
|
||||
const StackName = "project"
|
||||
|
||||
const (
|
||||
// https://cloud.google.com/resource-manager/reference/rest/v1/projects
|
||||
projectIDMaxLength = 30
|
||||
projectIDRandomizedSuffixByteLength = 2 // real length 4
|
||||
projectIDMinRandomizedSuffixLength = 2
|
||||
)
|
||||
|
||||
// NewStack creates a stack that provisions a GCP project.
|
||||
func NewStack(stacks *stack.Set, vars Variables) (*CrossStackOutput, error) {
|
||||
stack, locals, err := stacks.New(StackName,
|
||||
randomprovider.With(),
|
||||
// ID is not known ahead of time, we can omit it
|
||||
// The project we want might not exist yet, so omit it when initializing
|
||||
// the provider.
|
||||
googleprovider.With(""))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Name all stack resources after the desired project ID
|
||||
id := resourceid.New(vars.ProjectIDPrefix)
|
||||
|
||||
// The project ID must leave room for a randomized suffix and a separator.
|
||||
suffixByteLength := projectIDRandomizedSuffixByteLength
|
||||
if vars.ProjectIDSuffixLength != nil {
|
||||
suffixByteLength = *vars.ProjectIDSuffixLength / 2
|
||||
}
|
||||
realSuffixLength := suffixByteLength * 2 // after converting to hex
|
||||
if afterSuffixLength := len(vars.ProjectIDPrefix) + 1 + realSuffixLength; afterSuffixLength > projectIDMaxLength {
|
||||
return nil, errors.Newf("project ID prefix %q is too long after adding random suffix (%d characters) - got %d characters, but maximum is %d characters",
|
||||
vars.ProjectIDPrefix, projectIDRandomizedSuffixByteLength, afterSuffixLength, projectIDMaxLength)
|
||||
}
|
||||
projectID := random.New(stack, id, random.Config{
|
||||
ByteLength: suffixByteLength,
|
||||
Prefix: vars.ProjectIDPrefix,
|
||||
})
|
||||
id := resourceid.New(vars.ProjectID)
|
||||
|
||||
project := project.NewProject(stack,
|
||||
id.TerraformID("project"),
|
||||
&project.ProjectConfig{
|
||||
Name: pointers.Ptr(vars.DisplayName),
|
||||
ProjectId: &projectID.HexValue,
|
||||
ProjectId: &vars.ProjectID,
|
||||
AutoCreateNetwork: false,
|
||||
BillingAccount: pointers.Ptr(BillingAccountID),
|
||||
FolderId: func() *string {
|
||||
|
||||
@ -79,17 +79,11 @@ func (r *Renderer) RenderEnvironment(
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
var (
|
||||
projectIDPrefix = fmt.Sprintf("%s-%s", svc.ID, env.ID)
|
||||
stacks = stack.NewSet(r.OutputDir, stackSetOptions...)
|
||||
)
|
||||
stacks := stack.NewSet(r.OutputDir, stackSetOptions...)
|
||||
|
||||
// Render all required CDKTF stacks for this environment
|
||||
projectOutput, err := project.NewStack(stacks, project.Variables{
|
||||
ProjectIDPrefix: projectIDPrefix,
|
||||
ProjectIDSuffixLength: svc.ProjectIDSuffixLength,
|
||||
|
||||
ProjectID: env.ProjectID,
|
||||
DisplayName: fmt.Sprintf("%s - %s",
|
||||
pointers.Deref(svc.Name, svc.ID), env.ID),
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ go_library(
|
||||
"build.go",
|
||||
"environment.go",
|
||||
"monitoring.go",
|
||||
"projectid.go",
|
||||
"service.go",
|
||||
"spec.go",
|
||||
],
|
||||
@ -17,7 +18,7 @@ go_library(
|
||||
"//lib/errors",
|
||||
"//lib/pointers",
|
||||
"@com_github_grafana_regexp//:regexp",
|
||||
"@io_k8s_sigs_yaml//:yaml",
|
||||
"@in_gopkg_yaml_v3//:yaml_v3",
|
||||
],
|
||||
)
|
||||
|
||||
@ -25,6 +26,7 @@ go_test(
|
||||
name = "spec_test",
|
||||
srcs = [
|
||||
"environment_test.go",
|
||||
"projectid_test.go",
|
||||
"service_test.go",
|
||||
],
|
||||
embed = [":spec"],
|
||||
@ -32,5 +34,6 @@ go_test(
|
||||
"//lib/pointers",
|
||||
"@com_github_hexops_autogold_v2//:autogold",
|
||||
"@com_github_stretchr_testify//assert",
|
||||
"@com_github_stretchr_testify//require",
|
||||
],
|
||||
)
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
package spec
|
||||
|
||||
type BuildSpec struct {
|
||||
Image string `json:"image"`
|
||||
Source BuildSourceSpec `json:"source"`
|
||||
Image string `yaml:"image"`
|
||||
Source BuildSourceSpec `yaml:"source"`
|
||||
}
|
||||
|
||||
func (s BuildSpec) Validate() []error {
|
||||
@ -12,6 +12,6 @@ func (s BuildSpec) Validate() []error {
|
||||
}
|
||||
|
||||
type BuildSourceSpec struct {
|
||||
Repo string `json:"repo"`
|
||||
Dir string `json:"dir"`
|
||||
Repo string `yaml:"repo"`
|
||||
Dir string `yaml:"dir"`
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/dev/managedservicesplatform/internal/imageupdater"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
@ -14,28 +15,39 @@ import (
|
||||
type EnvironmentSpec struct {
|
||||
// ID is an all-lowercase alphanumeric identifier for the deployment
|
||||
// environment, e.g. "prod" or "dev".
|
||||
ID string `json:"id"`
|
||||
ID string `yaml:"id"`
|
||||
|
||||
// ProjectID is the generated Google Project ID for this environment,
|
||||
// provided by either 'sg msp init' or 'sg msp init -env'.
|
||||
//
|
||||
// The format is:
|
||||
//
|
||||
// $SERVICE_ID-$ENVIRONMENT_ID-$RANDOM_SUFFIX
|
||||
//
|
||||
// ❗ This value cannot be changed after your environment has been
|
||||
// initialized!
|
||||
ProjectID string `yaml:"projectID"`
|
||||
|
||||
// Category is either "test", "internal", or "external".
|
||||
Category *EnvironmentCategory `json:"category,omitempty"`
|
||||
Category *EnvironmentCategory `yaml:"category,omitempty"`
|
||||
|
||||
// Deploy specifies how to deploy revisions.
|
||||
Deploy EnvironmentDeploySpec `json:"deploy"`
|
||||
Deploy EnvironmentDeploySpec `yaml:"deploy"`
|
||||
|
||||
// EnvironmentServiceSpec carries service-specific configuration.
|
||||
*EnvironmentServiceSpec `json:",inline"`
|
||||
*EnvironmentServiceSpec `yaml:",inline"`
|
||||
// EnvironmentJobSpec carries job-specific configuration.
|
||||
*EnvironmentJobSpec `json:",inline"`
|
||||
*EnvironmentJobSpec `yaml:",inline"`
|
||||
|
||||
// Instances describes how machines running the service are deployed.
|
||||
Instances EnvironmentInstancesSpec `json:"instances"`
|
||||
Instances EnvironmentInstancesSpec `yaml:"instances"`
|
||||
|
||||
// Env are key-value pairs of environment variables to set on the service.
|
||||
//
|
||||
// Values can be subsituted with supported runtime values with gotemplate, e.g., "{{ .ProjectID }}"
|
||||
// - ProjectID: The project ID of the service.
|
||||
// - ServiceDnsName: The DNS name of the service.
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
Env map[string]string `yaml:"env,omitempty"`
|
||||
|
||||
// SecretEnv are key-value pairs of environment variables sourced from
|
||||
// secrets set on the service, where the value is the name of the secret
|
||||
@ -44,14 +56,26 @@ type EnvironmentSpec struct {
|
||||
// To point to a secret in another project, use the format
|
||||
// 'projects/{project}/secrets/{secretName}' in the value. Access to the
|
||||
// target project will be automatically granted.
|
||||
SecretEnv map[string]string `json:"secretEnv,omitempty"`
|
||||
SecretEnv map[string]string `yaml:"secretEnv,omitempty"`
|
||||
|
||||
// Resources configures additional resources that a service may depend on.
|
||||
Resources *EnvironmentResourcesSpec `json:"resources,omitempty"`
|
||||
Resources *EnvironmentResourcesSpec `yaml:"resources,omitempty"`
|
||||
}
|
||||
|
||||
func (s EnvironmentSpec) Validate() []error {
|
||||
var errs []error
|
||||
|
||||
if s.ProjectID == "" {
|
||||
errs = append(errs, errors.New("projectID is required"))
|
||||
}
|
||||
if len(s.ProjectID) > 30 {
|
||||
errs = append(errs, errors.New("projectID must be less than 30 characters"))
|
||||
}
|
||||
if !strings.Contains(s.ProjectID, fmt.Sprintf("-%s-", s.ID)) {
|
||||
errs = append(errs, errors.Newf("projectID %q must contain environment ID: expecting format '$SERVICE_ID-$ENVIRONMENT_ID-$RANDOM_SUFFIX'",
|
||||
s.ProjectID))
|
||||
}
|
||||
|
||||
errs = append(errs, s.Deploy.Validate()...)
|
||||
errs = append(errs, s.Resources.Validate()...)
|
||||
return errs
|
||||
@ -70,9 +94,9 @@ const (
|
||||
)
|
||||
|
||||
type EnvironmentDeploySpec struct {
|
||||
Type EnvironmentDeployType `json:"type"`
|
||||
Manual *EnvironmentDeployManualSpec `json:"manual,omitempty"`
|
||||
Subscription *EnvironmentDeployTypeSubscriptionSpec `json:"subscription,omitempty"`
|
||||
Type EnvironmentDeployType `yaml:"type"`
|
||||
Manual *EnvironmentDeployManualSpec `yaml:"manual,omitempty"`
|
||||
Subscription *EnvironmentDeployTypeSubscriptionSpec `yaml:"subscription,omitempty"`
|
||||
}
|
||||
|
||||
func (s EnvironmentDeploySpec) Validate() []error {
|
||||
@ -123,12 +147,12 @@ func (d EnvironmentDeploySpec) ResolveTag(repo string) (string, error) {
|
||||
|
||||
type EnvironmentDeployManualSpec struct {
|
||||
// Tag is the tag to deploy. If empty, defaults to "insiders".
|
||||
Tag string `json:"tag,omitempty"`
|
||||
Tag string `yaml:"tag,omitempty"`
|
||||
}
|
||||
|
||||
type EnvironmentDeployTypeSubscriptionSpec struct {
|
||||
// Tag is the tag to subscribe to.
|
||||
Tag string `json:"tag,omitempty"`
|
||||
Tag string `yaml:"tag,omitempty"`
|
||||
// TODO: In the future, we may support subscribing by semver constraints.
|
||||
}
|
||||
|
||||
@ -136,18 +160,18 @@ type EnvironmentServiceSpec struct {
|
||||
// Domain configures where the resource is externally accessible.
|
||||
//
|
||||
// Only supported for services of 'kind: service'.
|
||||
Domain *EnvironmentServiceDomainSpec `json:"domain,omitempty"`
|
||||
Domain *EnvironmentServiceDomainSpec `yaml:"domain,omitempty"`
|
||||
// StatupProbe is provisioned by default. It can be disabled with the
|
||||
// 'disabled' field. Probes are made to the MSP-standard '/-/healthz'
|
||||
// endpoint.
|
||||
//
|
||||
// Only supported for services of 'kind: service'.
|
||||
StatupProbe *EnvironmentServiceStartupProbeSpec `json:"startupProbe,omitempty"`
|
||||
StatupProbe *EnvironmentServiceStartupProbeSpec `yaml:"startupProbe,omitempty"`
|
||||
// LivenessProbe is only provisioned if this field is set. Probes are made
|
||||
// to the MSP-standard '/-/healthz' endpoint.
|
||||
//
|
||||
// Only supported for services of 'kind: service'.
|
||||
LivenessProbe *EnvironmentServiceLivenessProbeSpec `json:"livenessProbe,omitempty"`
|
||||
LivenessProbe *EnvironmentServiceLivenessProbeSpec `yaml:"livenessProbe,omitempty"`
|
||||
// Authentication configures access to the service. By default, the service
|
||||
// is publically available, and the service should handle any required
|
||||
// authentication by itself. Set this field to an empty value to not
|
||||
@ -161,13 +185,13 @@ type EnvironmentServiceSpec struct {
|
||||
// https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/cloud_run_v2_service_iam
|
||||
//
|
||||
// Only supported for services of 'kind: service'.
|
||||
Authentication *EnvironmentServiceAuthenticationSpec `json:"authentication,omitempty"`
|
||||
Authentication *EnvironmentServiceAuthenticationSpec `yaml:"authentication,omitempty"`
|
||||
}
|
||||
|
||||
type EnvironmentServiceDomainSpec struct {
|
||||
// Type is one of 'none' or 'cloudflare'. If empty, defaults to 'none'.
|
||||
Type EnvironmentDomainType `json:"type"`
|
||||
Cloudflare *EnvironmentDomainCloudflareSpec `json:"cloudflare,omitempty"`
|
||||
Type EnvironmentDomainType `yaml:"type"`
|
||||
Cloudflare *EnvironmentDomainCloudflareSpec `yaml:"cloudflare,omitempty"`
|
||||
}
|
||||
|
||||
// GetDNSName generates the DNS name for the environment. If nil or not configured,
|
||||
@ -190,29 +214,29 @@ const (
|
||||
)
|
||||
|
||||
type EnvironmentDomainCloudflareSpec struct {
|
||||
Subdomain string `json:"subdomain"`
|
||||
Zone string `json:"zone"`
|
||||
Subdomain string `yaml:"subdomain"`
|
||||
Zone string `yaml:"zone"`
|
||||
|
||||
// Proxied configures whether Cloudflare should proxy all traffic to get
|
||||
// WAF protection instead of only DNS resolution.
|
||||
Proxied bool `json:"proxied,omitempty"`
|
||||
Proxied bool `yaml:"proxied,omitempty"`
|
||||
|
||||
// Required configures whether traffic can only be allowed through Cloudflare.
|
||||
// TODO: Unimplemented.
|
||||
Required bool `json:"required,omitempty"`
|
||||
Required bool `yaml:"required,omitempty"`
|
||||
}
|
||||
|
||||
type EnvironmentInstancesSpec struct {
|
||||
Resources EnvironmentInstancesResourcesSpec `json:"resources"`
|
||||
Resources EnvironmentInstancesResourcesSpec `yaml:"resources"`
|
||||
// Scaling specifies the scaling behavior of the service.
|
||||
//
|
||||
// Currently only used for services of 'kind: service'.
|
||||
Scaling *EnvironmentInstancesScalingSpec `json:"scaling,omitempty"`
|
||||
Scaling *EnvironmentInstancesScalingSpec `yaml:"scaling,omitempty"`
|
||||
}
|
||||
|
||||
type EnvironmentInstancesResourcesSpec struct {
|
||||
CPU int `json:"cpu"`
|
||||
Memory string `json:"memory"`
|
||||
CPU int `yaml:"cpu"`
|
||||
Memory string `yaml:"memory"`
|
||||
}
|
||||
|
||||
type EnvironmentInstancesScalingSpec struct {
|
||||
@ -221,15 +245,15 @@ type EnvironmentInstancesScalingSpec struct {
|
||||
// Cloud Run will begin scaling up additional instances, up to MaxCount.
|
||||
//
|
||||
// If not provided, the defualt is defaultMaxConcurrentRequests
|
||||
MaxRequestConcurrency *int `json:"maxRequestConcurrency,omitempty"`
|
||||
MaxRequestConcurrency *int `yaml:"maxRequestConcurrency,omitempty"`
|
||||
// MinCount is the minimum number of instances that will be running at all
|
||||
// times. Set this to >0 to avoid service warm-up delays.
|
||||
MinCount int `json:"minCount"`
|
||||
MinCount int `yaml:"minCount"`
|
||||
// MaxCount is the maximum number of instances that Cloud Run is allowed to
|
||||
// scale up to.
|
||||
//
|
||||
// If not provided, the default is 5.
|
||||
MaxCount *int `json:"maxCount,omitempty"`
|
||||
MaxCount *int `yaml:"maxCount,omitempty"`
|
||||
}
|
||||
|
||||
type EnvironmentServiceLivenessProbeSpec struct {
|
||||
@ -237,18 +261,18 @@ type EnvironmentServiceLivenessProbeSpec struct {
|
||||
// in seconds.
|
||||
//
|
||||
// Defaults to 1 second.
|
||||
Timeout *int `json:"timeout,omitempty"`
|
||||
Timeout *int `yaml:"timeout,omitempty"`
|
||||
// Interval configures the interval, in seconds, at which to
|
||||
// probe the deployed service.
|
||||
//
|
||||
// Defaults to 1 second.
|
||||
Interval *int `json:"interval,omitempty"`
|
||||
Interval *int `yaml:"interval,omitempty"`
|
||||
}
|
||||
|
||||
type EnvironmentServiceAuthenticationSpec struct {
|
||||
// Sourcegraph enables access to everyone in the sourcegraph.com GSuite
|
||||
// domain.
|
||||
Sourcegraph *bool `json:"sourcegraph,omitempty"`
|
||||
Sourcegraph *bool `yaml:"sourcegraph,omitempty"`
|
||||
}
|
||||
|
||||
type EnvironmentServiceStartupProbeSpec struct {
|
||||
@ -258,32 +282,32 @@ type EnvironmentServiceStartupProbeSpec struct {
|
||||
//
|
||||
// This prevents the first Terraform apply from failing if your healthcheck
|
||||
// is comprehensive.
|
||||
Disabled *bool `json:"disabled,omitempty"`
|
||||
Disabled *bool `yaml:"disabled,omitempty"`
|
||||
|
||||
// Timeout configures the period of time after which the probe times out,
|
||||
// in seconds.
|
||||
//
|
||||
// Defaults to 1 second.
|
||||
Timeout *int `json:"timeout,omitempty"`
|
||||
Timeout *int `yaml:"timeout,omitempty"`
|
||||
// Interval configures the interval, in seconds, at which to
|
||||
// probe the deployed service.
|
||||
//
|
||||
// Defaults to 1 second.
|
||||
Interval *int `json:"interval,omitempty"`
|
||||
Interval *int `yaml:"interval,omitempty"`
|
||||
}
|
||||
|
||||
type EnvironmentJobSpec struct {
|
||||
// Schedule configures a cron schedule for the service.
|
||||
//
|
||||
// Only supported for services of 'kind: job'.
|
||||
Schedule *EnvironmentJobScheduleSpec `json:"schedule,omitempty"`
|
||||
Schedule *EnvironmentJobScheduleSpec `yaml:"schedule,omitempty"`
|
||||
}
|
||||
|
||||
type EnvironmentJobScheduleSpec struct {
|
||||
// Cron is a cron schedule in the form of "* * * * *".
|
||||
Cron string `json:"cron"`
|
||||
Cron string `yaml:"cron"`
|
||||
// Deadline of each attempt, in seconds.
|
||||
Deadline *int `json:"deadline,omitempty"`
|
||||
Deadline *int `yaml:"deadline,omitempty"`
|
||||
}
|
||||
|
||||
type EnvironmentResourcesSpec struct {
|
||||
@ -295,13 +319,13 @@ type EnvironmentResourcesSpec struct {
|
||||
//
|
||||
// Sourcegraph Redis libraries (i.e. internal/redispool) will automatically
|
||||
// use the given configuration.
|
||||
Redis *EnvironmentResourceRedisSpec `json:"redis,omitempty"`
|
||||
Redis *EnvironmentResourceRedisSpec `yaml:"redis,omitempty"`
|
||||
// PostgreSQL, if provided, provisions a PostgreSQL database instance backed
|
||||
// by Cloud SQL.
|
||||
//
|
||||
// To connect to the database, use
|
||||
// (lib/managedservicesplatform/service.Contract).GetPostgreSQLDB().
|
||||
PostgreSQL *EnvironmentResourcePostgreSQLSpec `json:"postgreSQL,omitempty"`
|
||||
PostgreSQL *EnvironmentResourcePostgreSQLSpec `yaml:"postgreSQL,omitempty"`
|
||||
// BigQueryDataset, if provided, provisions a dataset for the service to write
|
||||
// to. Details for writing to the dataset are automatically provided in
|
||||
// environment variables:
|
||||
@ -311,7 +335,7 @@ type EnvironmentResourcesSpec struct {
|
||||
//
|
||||
// Only one dataset can be provisioned using MSP per MSP service, but the
|
||||
// dataset may contain more than one table.
|
||||
BigQueryDataset *EnvironmentResourceBigQueryDatasetSpec `json:"bigQueryDataset,omitempty"`
|
||||
BigQueryDataset *EnvironmentResourceBigQueryDatasetSpec `yaml:"bigQueryDataset,omitempty"`
|
||||
}
|
||||
|
||||
func (s *EnvironmentResourcesSpec) Validate() []error {
|
||||
@ -326,19 +350,19 @@ func (s *EnvironmentResourcesSpec) Validate() []error {
|
||||
|
||||
type EnvironmentResourceRedisSpec struct {
|
||||
// Defaults to STANDARD_HA.
|
||||
Tier *string `json:"tier,omitempty"`
|
||||
Tier *string `yaml:"tier,omitempty"`
|
||||
// Defaults to 1.
|
||||
MemoryGB *int `json:"memoryGB,omitempty"`
|
||||
MemoryGB *int `yaml:"memoryGB,omitempty"`
|
||||
}
|
||||
|
||||
type EnvironmentResourcePostgreSQLSpec struct {
|
||||
// Databases to provision - required.
|
||||
Databases []string `json:"databases"`
|
||||
Databases []string `yaml:"databases"`
|
||||
// Defaults to 1. Must be 1, or an even number between 2 and 96.
|
||||
CPU *int `json:"cpu,omitempty"`
|
||||
CPU *int `yaml:"cpu,omitempty"`
|
||||
// Defaults to 4 (to meet CloudSQL minimum). You must request 0.9 to 6.5 GB
|
||||
// per vCPU.
|
||||
MemoryGB *int `json:"memoryGB,omitempty"`
|
||||
MemoryGB *int `yaml:"memoryGB,omitempty"`
|
||||
}
|
||||
|
||||
func (s *EnvironmentResourcePostgreSQLSpec) Validate() []error {
|
||||
@ -380,7 +404,7 @@ type EnvironmentResourceBigQueryDatasetSpec struct {
|
||||
// service specification file, in `${tableID}.bigquerytable.json`. Learn
|
||||
// more about BigQuery table schemas here:
|
||||
// https://cloud.google.com/bigquery/docs/schemas#specifying_a_json_schema_file
|
||||
Tables []string `json:"tables"`
|
||||
Tables []string `yaml:"tables"`
|
||||
// rawSchemaFiles are the `${tableID}.bigquerytable.json` files adjacent
|
||||
// to the service specification.
|
||||
// Loaded by (EnvironmentResourceBigQueryTableSpec).LoadSchemas().
|
||||
@ -390,14 +414,14 @@ type EnvironmentResourceBigQueryDatasetSpec struct {
|
||||
// into. By default, we use the service ID as the dataset ID.
|
||||
//
|
||||
// Dataset IDs must be alphanumeric (plus underscores).
|
||||
DatasetID *string `json:"datasetID,omitempty"`
|
||||
DatasetID *string `yaml:"datasetID,omitempty"`
|
||||
// ProjectID can be used to specify a separate project ID from the service's
|
||||
// project for BigQuery resources. If not provided, resources are created
|
||||
// within the service's project.
|
||||
ProjectID *string `json:"projectID,omitempty"`
|
||||
ProjectID *string `yaml:"projectID,omitempty"`
|
||||
// Location defaults to "US". Do not configure unless you know what you are
|
||||
// doing, as BigQuery locations are not the same as standard GCP regions.
|
||||
Location *string `json:"region,omitempty"`
|
||||
Location *string `yaml:"region,omitempty"`
|
||||
}
|
||||
|
||||
func (s *EnvironmentResourceBigQueryDatasetSpec) Validate() []error {
|
||||
|
||||
@ -12,7 +12,7 @@ var codeClassPattern = regexp.MustCompile(`\dx+`)
|
||||
|
||||
type MonitoringSpec struct {
|
||||
// Alerts is a list of alert configurations for the deployment
|
||||
Alerts MonitoringAlertsSpec `json:"alerts"`
|
||||
Alerts MonitoringAlertsSpec `yaml:"alerts"`
|
||||
}
|
||||
|
||||
func (s *MonitoringSpec) Validate() []error {
|
||||
@ -25,18 +25,18 @@ func (s *MonitoringSpec) Validate() []error {
|
||||
}
|
||||
|
||||
type MonitoringAlertsSpec struct {
|
||||
ResponseCodeRatios []ResponseCodeRatioSpec `json:"responseCodeRatios"`
|
||||
ResponseCodeRatios []ResponseCodeRatioSpec `yaml:"responseCodeRatios"`
|
||||
}
|
||||
|
||||
type ResponseCodeRatioSpec struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Code *int `json:"code,omitempty"`
|
||||
CodeClass *string `json:"codeClass,omitempty"`
|
||||
ExcludeCodes []string `json:"excludeCodes,omitempty"`
|
||||
Duration *string `json:"duration,omitempty"`
|
||||
Ratio float64 `json:"ratio"`
|
||||
ID string `yaml:"id"`
|
||||
Name string `yaml:"name"`
|
||||
Description *string `yaml:"description,omitempty"`
|
||||
Code *int `yaml:"code,omitempty"`
|
||||
CodeClass *string `yaml:"codeClass,omitempty"`
|
||||
ExcludeCodes []string `yaml:"excludeCodes,omitempty"`
|
||||
Duration *string `yaml:"duration,omitempty"`
|
||||
Ratio float64 `yaml:"ratio"`
|
||||
}
|
||||
|
||||
func (s *MonitoringAlertsSpec) Validate() []error {
|
||||
|
||||
41
dev/managedservicesplatform/spec/projectid.go
Normal file
41
dev/managedservicesplatform/spec/projectid.go
Normal file
@ -0,0 +1,41 @@
|
||||
package spec
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
)
|
||||
|
||||
const DefaultSuffixLength = 4
|
||||
|
||||
// NewProjectID generates a MSP-standard project ID for a service environment.
|
||||
func NewProjectID(serviceID, envID string, suffixLength int) (string, error) {
|
||||
if suffixLength < 2 {
|
||||
return "", errors.New("suffix length must be at least 2 characters long")
|
||||
}
|
||||
|
||||
suffix, err := newRandomAlphabeticalString(suffixLength)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "generate suffix")
|
||||
}
|
||||
projectID := fmt.Sprintf("%s-%s-%s",
|
||||
serviceID, envID, suffix)
|
||||
|
||||
// https://cloud.google.com/resource-manager/reference/rest/v1/projects
|
||||
if len(projectID) > 30 {
|
||||
return "", errors.Newf("project ID %q must be no longer than 30 characters, try a shorter service ID or environment ID")
|
||||
}
|
||||
|
||||
return projectID, nil
|
||||
}
|
||||
|
||||
func newRandomAlphabeticalString(length int) (string, error) {
|
||||
buf := make([]byte, length)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
return "", errors.Wrap(err, "generate random string")
|
||||
}
|
||||
// Base 64 can be longer than len
|
||||
return hex.EncodeToString(buf)[:length], nil
|
||||
}
|
||||
29
dev/managedservicesplatform/spec/projectid_test.go
Normal file
29
dev/managedservicesplatform/spec/projectid_test.go
Normal file
@ -0,0 +1,29 @@
|
||||
package spec
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewProjectID(t *testing.T) {
|
||||
seenID := make(map[string]struct{})
|
||||
const (
|
||||
serviceID = "msp-test"
|
||||
envID = "dev"
|
||||
)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
id, err := NewProjectID(serviceID, envID, DefaultSuffixLength)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Contains(t, id, serviceID)
|
||||
assert.Contains(t, id, envID)
|
||||
|
||||
_, seenBefore := seenID[id]
|
||||
assert.False(t, seenBefore, id)
|
||||
|
||||
seenID[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
@ -10,29 +10,25 @@ import (
|
||||
type ServiceSpec struct {
|
||||
// ID is an all-lowercase, hyphen-delimited identifier for the service,
|
||||
// e.g. "cody-gateway". It MUST be at most 20 characters long.
|
||||
ID string `json:"id"`
|
||||
ID string `yaml:"id"`
|
||||
// Name is an optional human-readable display name for the service,
|
||||
// e.g. "Cody Gateway".
|
||||
Name *string `json:"name"`
|
||||
Name *string `yaml:"name"`
|
||||
// Owners denotes the teams or individuals primarily responsible for the
|
||||
// service.
|
||||
Owners []string `json:"owners"`
|
||||
Owners []string `yaml:"owners"`
|
||||
|
||||
// Kind is the type of the service, either 'service' or 'job'. Defaults to
|
||||
// 'service'.
|
||||
Kind *ServiceKind `json:"kind,omitempty"`
|
||||
Kind *ServiceKind `yaml:"kind,omitempty"`
|
||||
// Protocol is a protocol other than HTTP that the service communicates
|
||||
// with. If empty, the service uses HTTP. To use gRPC, configure 'h2c':
|
||||
// https://cloud.google.com/run/docs/configuring/http2
|
||||
Protocol *ServiceProtocol `json:"protocol,omitempty"`
|
||||
|
||||
// ProjectIDSuffixLength can be configured to truncate the length of the
|
||||
// service's generated project IDs.
|
||||
ProjectIDSuffixLength *int `json:"projectIDSuffixLength,omitempty"`
|
||||
Protocol *ServiceProtocol `yaml:"protocol,omitempty"`
|
||||
|
||||
// IAM is an optional IAM configuration for the service account on the
|
||||
// service's GCP project.
|
||||
IAM *ServiceIAMSpec `json:"iam,omitempty"`
|
||||
IAM *ServiceIAMSpec `yaml:"iam,omitempty"`
|
||||
}
|
||||
|
||||
func (s ServiceSpec) Validate() []error {
|
||||
@ -42,15 +38,10 @@ func (s ServiceSpec) Validate() []error {
|
||||
errs = append(errs, errors.New("id must be at most 20 characters"))
|
||||
}
|
||||
|
||||
if s.ProjectIDSuffixLength != nil && *s.ProjectIDSuffixLength < 4 {
|
||||
errs = append(errs, errors.New("projectIDSuffixLength must be >= 4"))
|
||||
}
|
||||
|
||||
if s.IAM != nil {
|
||||
errs = append(errs, s.IAM.Validate()...)
|
||||
}
|
||||
|
||||
// TODO: Add validation
|
||||
return errs
|
||||
}
|
||||
|
||||
@ -72,15 +63,15 @@ func (s *ServiceKind) Is(kind ServiceKind) bool {
|
||||
|
||||
type ServiceIAMSpec struct {
|
||||
// Services is a list of GCP services to enable in the service's project.
|
||||
Services []string `json:"services,omitempty"`
|
||||
Services []string `yaml:"services,omitempty"`
|
||||
|
||||
// Roles is a list of IAM roles to grant to the service account.
|
||||
Roles []string `json:"roles,omitempty"`
|
||||
Roles []string `yaml:"roles,omitempty"`
|
||||
// Permissions is a list of IAM permissions to grant to the service account.
|
||||
//
|
||||
// MSP will create a custom role with these permissions and grant it to the
|
||||
// service account.
|
||||
Permissions []string `json:"permissions,omitempty"`
|
||||
Permissions []string `yaml:"permissions,omitempty"`
|
||||
}
|
||||
|
||||
func (s ServiceIAMSpec) Validate() []error {
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
package spec
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
// We intentionally use sigs.k8s.io/yaml because it has some convenience features,
|
||||
// and nicer formatting. We use this in Sourcegraph Cloud as well.
|
||||
"sigs.k8s.io/yaml"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
"github.com/sourcegraph/sourcegraph/lib/pointers"
|
||||
@ -24,10 +24,10 @@ import (
|
||||
// Package dev/managedservicesplatform handles generating Terraform manifests
|
||||
// from a given spec.
|
||||
type Spec struct {
|
||||
Service ServiceSpec `json:"service"`
|
||||
Build BuildSpec `json:"build"`
|
||||
Environments []EnvironmentSpec `json:"environments"`
|
||||
Monitoring *MonitoringSpec `json:"monitoring,omitempty"`
|
||||
Service ServiceSpec `yaml:"service"`
|
||||
Build BuildSpec `yaml:"build"`
|
||||
Environments []EnvironmentSpec `yaml:"environments"`
|
||||
Monitoring *MonitoringSpec `yaml:"monitoring,omitempty"`
|
||||
}
|
||||
|
||||
// Open a specification file, validate it, unmarshal the data as a MSP spec,
|
||||
@ -55,6 +55,47 @@ func Open(specPath string) (*Spec, error) {
|
||||
return spec, nil
|
||||
}
|
||||
|
||||
// AppendEnvironment attaches environmentSpec, expressed as a map *yaml.Node,
|
||||
// to the spec's "environments" list. It returns the updated spec data. The
|
||||
// update preserves all formatting and docstrings.
|
||||
func AppendEnvironment(specData []byte, environmentSpec *yaml.Node) ([]byte, error) {
|
||||
if environmentSpec.Kind != yaml.ScalarNode && environmentSpec.Tag != "!!map" {
|
||||
return nil, errors.Newf("environment spec must be a YAML map node, got kind: %v, tag: %q",
|
||||
environmentSpec.Kind, environmentSpec.Tag)
|
||||
}
|
||||
|
||||
var doc yaml.Node
|
||||
if err := yaml.Unmarshal(specData, &doc); err != nil {
|
||||
return nil, errors.Wrap(err, "parse spec YAML")
|
||||
}
|
||||
|
||||
var added bool
|
||||
root := doc.Content[0]
|
||||
for i, n := range root.Content {
|
||||
if n.Value == "environments" {
|
||||
envList := root.Content[i+1]
|
||||
envList.Content = append(envList.Content, environmentSpec)
|
||||
added = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !added {
|
||||
return nil, errors.New("spec 'environments' field not found")
|
||||
}
|
||||
|
||||
// This is the only place we marshal a spec, other than the hand-written
|
||||
// templates in dev/sg/msp/example. We need to set up an encoder to align
|
||||
// with our preferences.
|
||||
var update bytes.Buffer
|
||||
enc := yaml.NewEncoder(&update)
|
||||
enc.SetIndent(2)
|
||||
if err := enc.Encode(&doc); err != nil {
|
||||
return nil, errors.Wrap(err, "render updated spec to YAML")
|
||||
}
|
||||
|
||||
return update.Bytes(), nil
|
||||
}
|
||||
|
||||
// parse validates and unmarshals data as a MSP spec.
|
||||
func parse(data []byte) (*Spec, error) {
|
||||
var s Spec
|
||||
@ -85,15 +126,20 @@ func (s Spec) Validate() []error {
|
||||
}
|
||||
}
|
||||
|
||||
for _, env := range s.ListEnvironmentIDs() {
|
||||
projectName := fmt.Sprintf("%s - %s",
|
||||
pointers.Deref(s.Service.Name, s.Service.ID), env)
|
||||
if len(projectName) > 30 {
|
||||
for _, env := range s.Environments {
|
||||
projectDisplayName := fmt.Sprintf("%s - %s",
|
||||
pointers.Deref(s.Service.Name, s.Service.ID), env.ID)
|
||||
if len(projectDisplayName) > 30 {
|
||||
errs = append(errs, errors.Newf(
|
||||
"full environment name %q exceeds 30 characters limit - try a shorter service name or environment ID",
|
||||
projectName,
|
||||
projectDisplayName,
|
||||
))
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(env.ProjectID, fmt.Sprintf("%s-", s.Service.ID)) {
|
||||
errs = append(errs, errors.Newf("environment %q projectID %q must contain service ID: expecting format '$SERVICE_ID-$ENVIRONMENT_ID-$RANDOM_SUFFIX'",
|
||||
env.ID, env.ProjectID))
|
||||
}
|
||||
}
|
||||
|
||||
errs = append(errs, s.Service.Validate()...)
|
||||
|
||||
@ -21,6 +21,7 @@ go_library(
|
||||
"//dev/sg/msp/example",
|
||||
"//dev/sg/msp/repo",
|
||||
"//dev/sg/msp/schema",
|
||||
"//lib/cliutil/completions",
|
||||
"//lib/errors",
|
||||
"//lib/output",
|
||||
"@com_github_urfave_cli_v2//:cli",
|
||||
|
||||
@ -7,11 +7,14 @@ go_library(
|
||||
embedsrcs = [
|
||||
"job.template.yaml",
|
||||
"service.template.yaml",
|
||||
"environment.template.yaml",
|
||||
],
|
||||
importpath = "github.com/sourcegraph/sourcegraph/dev/sg/msp/example",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//dev/managedservicesplatform/spec",
|
||||
"//lib/errors",
|
||||
"@in_gopkg_yaml_v3//:yaml_v3",
|
||||
"@org_golang_x_text//cases",
|
||||
"@org_golang_x_text//language",
|
||||
],
|
||||
@ -20,6 +23,7 @@ go_library(
|
||||
go_test(
|
||||
name = "example_test",
|
||||
srcs = ["example_test.go"],
|
||||
data = glob(["testdata/**"]),
|
||||
embed = [":example"],
|
||||
deps = [
|
||||
"//dev/managedservicesplatform/spec",
|
||||
|
||||
7
dev/sg/msp/example/environment.template.yaml
Normal file
7
dev/sg/msp/example/environment.template.yaml
Normal file
@ -0,0 +1,7 @@
|
||||
id: {{ .EnvironmentID }}
|
||||
projectID: {{ newProjectID .ServiceID .EnvironmentID .ProjectIDSuffixLength }}
|
||||
# TODO: We initially provision in 'test' to make it easy to access the project
|
||||
# during setup. Once done, you should change this to 'external' or 'internal'.
|
||||
category: test
|
||||
# TODO: Fill out the rest of your configuration here!
|
||||
# ...
|
||||
@ -8,10 +8,18 @@ import (
|
||||
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/dev/managedservicesplatform/spec"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
)
|
||||
|
||||
const newProjectIDFuncKey = "newProjectID"
|
||||
|
||||
var templateFuncs = template.FuncMap{
|
||||
newProjectIDFuncKey: spec.NewProjectID,
|
||||
}
|
||||
|
||||
type Template struct {
|
||||
// ID is spec.service.id - required.
|
||||
ID string
|
||||
@ -21,6 +29,9 @@ type Template struct {
|
||||
Owner string
|
||||
// Dev indicates if this template should render a dev environment.
|
||||
Dev bool
|
||||
// ProjectIDSuffixLength specifies the length of the generated project ID's
|
||||
// random suffix.
|
||||
ProjectIDSuffixLength int
|
||||
}
|
||||
|
||||
func (t *Template) setDefaults() {
|
||||
@ -33,7 +44,13 @@ func (t *Template) setDefaults() {
|
||||
var (
|
||||
//go:embed service.template.yaml
|
||||
serviceTemplateYAML string
|
||||
serviceTemplate = template.Must(template.New("service").Parse(serviceTemplateYAML))
|
||||
serviceTemplate = func() *template.Template {
|
||||
return template.Must(
|
||||
template.New("service").
|
||||
Funcs(templateFuncs).
|
||||
Parse(serviceTemplateYAML),
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
// NewService provides a simple MSP service specification.
|
||||
@ -41,7 +58,7 @@ func NewService(t Template) ([]byte, error) {
|
||||
t.setDefaults()
|
||||
|
||||
var b bytes.Buffer
|
||||
if err := serviceTemplate.Execute(&b, t); err != nil {
|
||||
if err := serviceTemplate().Execute(&b, t); err != nil {
|
||||
return nil, errors.Wrap(err, "executing template")
|
||||
}
|
||||
return b.Bytes(), nil
|
||||
@ -50,7 +67,13 @@ func NewService(t Template) ([]byte, error) {
|
||||
var (
|
||||
//go:embed job.template.yaml
|
||||
jobTemplateYAML string
|
||||
jobTemplate = template.Must(template.New("job").Parse(jobTemplateYAML))
|
||||
jobTemplate = func() *template.Template {
|
||||
return template.Must(
|
||||
template.New("job").
|
||||
Funcs(templateFuncs).
|
||||
Parse(jobTemplateYAML),
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
// NewJob provides a simple MSP job specification.
|
||||
@ -58,8 +81,43 @@ func NewJob(t Template) ([]byte, error) {
|
||||
t.setDefaults()
|
||||
|
||||
var b bytes.Buffer
|
||||
if err := jobTemplate.Execute(&b, t); err != nil {
|
||||
if err := jobTemplate().Execute(&b, t); err != nil {
|
||||
return nil, errors.Wrap(err, "executing template")
|
||||
}
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
var (
|
||||
//go:embed environment.template.yaml
|
||||
environmentTemplateYAML string
|
||||
environmentTemplate = func() *template.Template {
|
||||
return template.Must(
|
||||
template.New("environment").
|
||||
Funcs(templateFuncs).
|
||||
Parse(environmentTemplateYAML),
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
type EnvironmentTemplate struct {
|
||||
ServiceID string
|
||||
EnvironmentID string
|
||||
// ProjectIDSuffixLength is the length of the random suffix appended to
|
||||
// the generated project ID.
|
||||
ProjectIDSuffixLength int
|
||||
}
|
||||
|
||||
func NewEnvironment(t EnvironmentTemplate) (*yaml.Node, error) {
|
||||
var b bytes.Buffer
|
||||
if err := environmentTemplate().Execute(&b, t); err != nil {
|
||||
return nil, errors.Wrap(err, "executing template")
|
||||
}
|
||||
|
||||
var doc yaml.Node
|
||||
if err := yaml.Unmarshal(b.Bytes(), &doc); err != nil {
|
||||
return nil, errors.Wrap(err, "unmarshal template as YAML")
|
||||
}
|
||||
|
||||
root := doc.Content[0]
|
||||
return root, nil
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package example
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/hexops/autogold/v2"
|
||||
@ -11,20 +13,34 @@ import (
|
||||
"github.com/sourcegraph/sourcegraph/dev/managedservicesplatform/spec"
|
||||
)
|
||||
|
||||
func mockNewProjectID(t *testing.T) {
|
||||
templateFuncs[newProjectIDFuncKey] = func(s, e string, l int) (string, error) {
|
||||
if len(s) == 0 {
|
||||
return "", errors.New("service ID is required")
|
||||
}
|
||||
if len(e) == 0 {
|
||||
return "", errors.New("environment ID is required")
|
||||
}
|
||||
if l == 0 {
|
||||
return "", errors.New("expected length > 0")
|
||||
}
|
||||
return fmt.Sprintf("%s-%s-%s", s, e, t.Name()), nil
|
||||
}
|
||||
t.Cleanup(func() { templateFuncs[newProjectIDFuncKey] = spec.NewProjectID })
|
||||
}
|
||||
|
||||
func TestNewService(t *testing.T) {
|
||||
mockNewProjectID(t)
|
||||
|
||||
f, err := NewService(Template{
|
||||
ID: "msp-example",
|
||||
Dev: true,
|
||||
Owner: "core-services",
|
||||
|
||||
ProjectIDSuffixLength: 4,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("is valid", func(t *testing.T) {
|
||||
var s spec.Spec
|
||||
require.NoError(t, yaml.Unmarshal(f, &s))
|
||||
assert.Empty(t, s.Validate())
|
||||
})
|
||||
|
||||
autogold.Expect(`service:
|
||||
id: msp-example
|
||||
name: Msp Example
|
||||
@ -43,6 +59,7 @@ build:
|
||||
|
||||
environments:
|
||||
- id: dev
|
||||
projectID: msp-example-dev-TestNewService
|
||||
# TODO: We initially provision in 'test' to make it easy to access the project
|
||||
# during setup. Once done, you should change this to 'external' or 'internal'.
|
||||
category: test
|
||||
@ -73,15 +90,6 @@ environments:
|
||||
# Only enable if your service implements MSP /-/healthz conventions.
|
||||
disabled: true
|
||||
`).Equal(t, string(f))
|
||||
}
|
||||
|
||||
func TestNewJob(t *testing.T) {
|
||||
f, err := NewJob(Template{
|
||||
ID: "msp-example",
|
||||
Dev: true,
|
||||
Owner: "core-services",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("is valid", func(t *testing.T) {
|
||||
var s spec.Spec
|
||||
@ -89,6 +97,21 @@ func TestNewJob(t *testing.T) {
|
||||
assert.Empty(t, s.Validate())
|
||||
})
|
||||
|
||||
testInsertProdEnvironment(t, "msp-example", f)
|
||||
}
|
||||
|
||||
func TestNewJob(t *testing.T) {
|
||||
mockNewProjectID(t)
|
||||
|
||||
f, err := NewJob(Template{
|
||||
ID: "msp-example",
|
||||
Dev: true,
|
||||
Owner: "core-services",
|
||||
|
||||
ProjectIDSuffixLength: 4,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
autogold.Expect(`service:
|
||||
kind: job
|
||||
id: msp-example
|
||||
@ -108,6 +131,7 @@ build:
|
||||
|
||||
environments:
|
||||
- id: dev
|
||||
projectID: msp-example-dev-TestNewJob
|
||||
# TODO: We initially provision in 'test' to make it easy to access the project
|
||||
# during setup. Once done, you should change this to 'external' or 'internal'.
|
||||
category: test
|
||||
@ -130,4 +154,28 @@ environments:
|
||||
cpu: 1
|
||||
memory: 1Gi
|
||||
`).Equal(t, string(f))
|
||||
|
||||
t.Run("is valid", func(t *testing.T) {
|
||||
var s spec.Spec
|
||||
require.NoError(t, yaml.Unmarshal(f, &s))
|
||||
assert.Empty(t, s.Validate())
|
||||
})
|
||||
|
||||
testInsertProdEnvironment(t, "msp-example", f)
|
||||
}
|
||||
|
||||
func testInsertProdEnvironment(t *testing.T, serviceID string, specData []byte) {
|
||||
t.Run("testInsertProdEnvironment", func(t *testing.T) {
|
||||
e, err := NewEnvironment(EnvironmentTemplate{
|
||||
ServiceID: serviceID,
|
||||
EnvironmentID: "prod",
|
||||
ProjectIDSuffixLength: 4,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
updatedSpecData, err := spec.AppendEnvironment(specData, e)
|
||||
require.NoError(t, err)
|
||||
|
||||
autogold.ExpectFile(t, autogold.Raw(string(updatedSpecData)))
|
||||
})
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@ build:
|
||||
|
||||
environments:
|
||||
- id: {{ if .Dev }}dev{{ else }}prod{{ end }}
|
||||
projectID: {{ if .Dev }}{{ newProjectID .ID "dev" .ProjectIDSuffixLength }}{{ else }}{{ newProjectID .ID "dev" .ProjectIDSuffixLength }}{{ end }}
|
||||
# TODO: We initially provision in 'test' to make it easy to access the project
|
||||
# during setup. Once done, you should change this to 'external' or 'internal'.
|
||||
category: test
|
||||
|
||||
@ -16,6 +16,7 @@ build:
|
||||
|
||||
environments:
|
||||
- id: {{ if .Dev }}dev{{ else }}prod{{ end }}
|
||||
projectID: {{ if .Dev }}{{ newProjectID .ID "dev" .ProjectIDSuffixLength }}{{ else }}{{ newProjectID .ID "dev" .ProjectIDSuffixLength }}{{ end }}
|
||||
# TODO: We initially provision in 'test' to make it easy to access the project
|
||||
# during setup. Once done, you should change this to 'external' or 'internal'.
|
||||
category: test
|
||||
|
||||
46
dev/sg/msp/example/testdata/TestNewJob/testInsertProdEnvironment.golden
vendored
Normal file
46
dev/sg/msp/example/testdata/TestNewJob/testInsertProdEnvironment.golden
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
service:
|
||||
kind: job
|
||||
id: msp-example
|
||||
name: Msp Example
|
||||
owners:
|
||||
- core-services
|
||||
build:
|
||||
# TODO: Configure the correct image for your job here. If you use a private
|
||||
# registry like us.gcr.io or Artifact Registry, access will automatically be
|
||||
# granted for your job to pull the correct image.
|
||||
image: us.gcr.io/sourcegraph-dev/msp-example
|
||||
# TODO: Configure where the source code for your job lives here.
|
||||
source:
|
||||
repo: github.com/sourcegraph/sourcegraph
|
||||
dir: cmd/msp-example
|
||||
environments:
|
||||
- id: dev
|
||||
projectID: msp-example-dev-TestNewJob
|
||||
# TODO: We initially provision in 'test' to make it easy to access the project
|
||||
# during setup. Once done, you should change this to 'external' or 'internal'.
|
||||
category: test
|
||||
# Specify a strategy for updating the image.
|
||||
deploy:
|
||||
type: manual
|
||||
manual:
|
||||
tag: insiders
|
||||
# Specify the schedule at which to run your job.
|
||||
schedule:
|
||||
cron: 0 * * * *
|
||||
deadline: 600 # 10 minutes
|
||||
# Specify environment configuration your service needs to operate.
|
||||
env:
|
||||
SRC_LOG_LEVEL: info
|
||||
SRC_LOG_FORMAT: json_gcp
|
||||
# Specify the resources your job gets.
|
||||
instances:
|
||||
resources:
|
||||
cpu: 1
|
||||
memory: 1Gi
|
||||
- id: prod
|
||||
projectID: msp-example-prod-TestNewJob
|
||||
# TODO: We initially provision in 'test' to make it easy to access the project
|
||||
# during setup. Once done, you should change this to 'external' or 'internal'.
|
||||
category: test
|
||||
# TODO: Fill out the rest of your configuration here!
|
||||
# ...
|
||||
53
dev/sg/msp/example/testdata/TestNewService/testInsertProdEnvironment.golden
vendored
Normal file
53
dev/sg/msp/example/testdata/TestNewService/testInsertProdEnvironment.golden
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
service:
|
||||
id: msp-example
|
||||
name: Msp Example
|
||||
owners:
|
||||
- core-services
|
||||
build:
|
||||
# TODO: Configure the correct image for your service here. If you use a private
|
||||
# registry like us.gcr.io or Artifact Registry, access will automatically be
|
||||
# granted for your service to pull the correct image.
|
||||
image: us.gcr.io/sourcegraph-dev/msp-example
|
||||
# TODO: Configure where the source code for your service lives here.
|
||||
source:
|
||||
repo: github.com/sourcegraph/sourcegraph
|
||||
dir: cmd/msp-example
|
||||
environments:
|
||||
- id: dev
|
||||
projectID: msp-example-dev-TestNewService
|
||||
# TODO: We initially provision in 'test' to make it easy to access the project
|
||||
# during setup. Once done, you should change this to 'external' or 'internal'.
|
||||
category: test
|
||||
# Specify a deployment strategy for upgrades.
|
||||
deploy:
|
||||
type: manual
|
||||
manual:
|
||||
tag: insiders
|
||||
# Specify an externally facing domain.
|
||||
domain:
|
||||
type: cloudflare
|
||||
cloudflare:
|
||||
subdomain: msp-example
|
||||
zone: sgdev.org
|
||||
# Specify environment configuration your service needs to operate.
|
||||
env:
|
||||
SRC_LOG_LEVEL: info
|
||||
SRC_LOG_FORMAT: json_gcp
|
||||
# Specify how your service should scale.
|
||||
instances:
|
||||
resources:
|
||||
cpu: 1
|
||||
memory: 1Gi
|
||||
scaling:
|
||||
maxCount: 3
|
||||
minCount: 1
|
||||
startupProbe:
|
||||
# Only enable if your service implements MSP /-/healthz conventions.
|
||||
disabled: true
|
||||
- id: prod
|
||||
projectID: msp-example-prod-TestNewService
|
||||
# TODO: We initially provision in 'test' to make it easy to access the project
|
||||
# during setup. Once done, you should change this to 'external' or 'internal'.
|
||||
category: test
|
||||
# TODO: Fill out the rest of your configuration here!
|
||||
# ...
|
||||
@ -24,7 +24,9 @@ func Render() ([]byte, error) {
|
||||
return nil, errors.Wrap(err, "must be in sourcegraph/sourcegraph repository")
|
||||
}
|
||||
|
||||
var r jsonschema.Reflector
|
||||
r := jsonschema.Reflector{
|
||||
FieldNameTag: "yaml",
|
||||
}
|
||||
if err := r.AddGoComments(
|
||||
"github.com/sourcegraph/sourcegraph",
|
||||
"./dev/managedservicesplatform/spec",
|
||||
|
||||
@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/dev/managedservicesplatform"
|
||||
"github.com/sourcegraph/sourcegraph/dev/managedservicesplatform/googlesecretsmanager"
|
||||
"github.com/sourcegraph/sourcegraph/dev/managedservicesplatform/spec"
|
||||
"github.com/sourcegraph/sourcegraph/dev/managedservicesplatform/terraformcloud"
|
||||
"github.com/sourcegraph/sourcegraph/dev/sg/cloudsqlproxy"
|
||||
"github.com/sourcegraph/sourcegraph/dev/sg/internal/category"
|
||||
@ -21,6 +22,7 @@ import (
|
||||
"github.com/sourcegraph/sourcegraph/dev/sg/msp/example"
|
||||
msprepo "github.com/sourcegraph/sourcegraph/dev/sg/msp/repo"
|
||||
"github.com/sourcegraph/sourcegraph/dev/sg/msp/schema"
|
||||
"github.com/sourcegraph/sourcegraph/lib/cliutil/completions"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
)
|
||||
|
||||
@ -58,9 +60,8 @@ sg msp init -owner core-services -name "MSP Example Service" msp-example
|
||||
Value: "service",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "owner",
|
||||
Usage: "Name of team owning this new service",
|
||||
Required: true,
|
||||
Name: "owner",
|
||||
Usage: "Name of team owning this new service",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
@ -68,7 +69,12 @@ sg msp init -owner core-services -name "MSP Example Service" msp-example
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "dev",
|
||||
Usage: "Generate a dev environment as the initial environment",
|
||||
Usage: "Generate a dev environment",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "project-id-suffix-length",
|
||||
Usage: "Length of random suffix appended to generated project IDs",
|
||||
Value: spec.DefaultSuffixLength,
|
||||
},
|
||||
},
|
||||
Before: msprepo.UseManagedServicesRepo,
|
||||
@ -82,6 +88,8 @@ sg msp init -owner core-services -name "MSP Example Service" msp-example
|
||||
Name: c.String("name"),
|
||||
Owner: c.String("owner"),
|
||||
Dev: c.Bool("dev"),
|
||||
|
||||
ProjectIDSuffixLength: c.Int("project-id-suffix-length"),
|
||||
}
|
||||
|
||||
var exampleSpec []byte
|
||||
@ -114,6 +122,64 @@ sg msp init -owner core-services -name "MSP Example Service" msp-example
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "init-env",
|
||||
ArgsUsage: "<service ID> <env ID>",
|
||||
Usage: "Add an environment to an existing Managed Services Platform service",
|
||||
Flags: []cli.Flag{
|
||||
&cli.IntFlag{
|
||||
Name: "project-id-suffix-length",
|
||||
Usage: "Length of random suffix appended to generated project IDs",
|
||||
Value: spec.DefaultSuffixLength,
|
||||
},
|
||||
},
|
||||
Before: msprepo.UseManagedServicesRepo,
|
||||
BashComplete: completions.CompleteArgs(func() (options []string) {
|
||||
ss, _ := msprepo.ListServices()
|
||||
return ss
|
||||
}),
|
||||
Action: func(c *cli.Context) error {
|
||||
svc, err := useServiceArgument(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
envID := c.Args().Get(0)
|
||||
if envID == "" {
|
||||
return errors.New("second argument <environment ID> is required")
|
||||
}
|
||||
if existing := svc.GetEnvironment(envID); existing != nil {
|
||||
return errors.Newf("environment %q already exists", envID)
|
||||
}
|
||||
|
||||
envNode, err := example.NewEnvironment(example.EnvironmentTemplate{
|
||||
ServiceID: svc.Service.ID,
|
||||
EnvironmentID: envID,
|
||||
ProjectIDSuffixLength: c.Int("project-id-suffix-length"),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "example.NewEnvironment")
|
||||
}
|
||||
|
||||
specPath := msprepo.ServiceYAMLPath(msprepo.ServiceYAMLPath(svc.Service.ID))
|
||||
specData, err := os.ReadFile(specPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "ReadFile")
|
||||
}
|
||||
|
||||
specData, err = spec.AppendEnvironment(specData, envNode)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "spec.AppendEnvironment")
|
||||
}
|
||||
|
||||
if err := os.WriteFile(specPath, specData, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
std.Out.WriteSuccessf("Initialized environment %q in %s",
|
||||
envID, specPath)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "generate",
|
||||
Aliases: []string{"gen"},
|
||||
@ -208,31 +274,16 @@ sg msp generate -all <service>
|
||||
},
|
||||
BashComplete: msprepo.ServicesAndEnvironmentsCompletion(),
|
||||
Action: func(c *cli.Context) error {
|
||||
service, env, err := useServiceAndEnvironmentArguments(c)
|
||||
_, env, err := useServiceAndEnvironmentArguments(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tfcClient, err := getTFCRunsClient(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
projectOutputs, err := tfcClient.GetOutputs(c.Context,
|
||||
terraformcloud.WorkspaceName(service.Service, *env,
|
||||
managedservicesplatform.StackNameProject))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "get IAM outputs")
|
||||
}
|
||||
projectID, err := projectOutputs.Find("project_id")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "get project ID")
|
||||
}
|
||||
|
||||
switch component := c.String("component"); component {
|
||||
case "service":
|
||||
std.Out.WriteNoticef("Opening link to service logs in browser...")
|
||||
return open.URL((&gcplogurl.Explorer{
|
||||
ProjectID: projectID.Value.(string),
|
||||
ProjectID: env.ProjectID,
|
||||
Query: gcplogurl.Query(`resource.type = "cloud_run_revision" jsonPayload.InstrumentationScope != ""`),
|
||||
SummaryFields: &gcplogurl.SummaryFields{
|
||||
Fields: []string{
|
||||
|
||||
5
go.mod
5
go.mod
@ -266,7 +266,7 @@ require (
|
||||
github.com/hashicorp/cronexpr v1.1.1
|
||||
github.com/hashicorp/go-tfe v1.32.1
|
||||
github.com/hashicorp/terraform-cdk-go/cdktf v0.17.3
|
||||
github.com/invopop/jsonschema v0.7.0
|
||||
github.com/invopop/jsonschema v0.12.0
|
||||
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa
|
||||
github.com/mroth/weightedrand/v2 v2.0.1
|
||||
github.com/oschwald/maxminddb-golang v1.12.0
|
||||
@ -336,7 +336,6 @@ require (
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/hashicorp/jsonapi v0.0.0-20210826224640-ee7dae0fb22d // indirect
|
||||
github.com/iancoleman/orderedmap v0.2.0 // indirect
|
||||
github.com/iancoleman/strcase v0.3.0 // indirect
|
||||
github.com/jackc/pgproto3/v2 v2.3.2 // indirect
|
||||
github.com/jackc/pgx/v5 v5.5.0 // indirect
|
||||
@ -575,7 +574,7 @@ require (
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.5
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8
|
||||
github.com/xanzy/go-gitlab v0.86.0
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
|
||||
12
go.sum
12
go.sum
@ -1065,9 +1065,6 @@ github.com/honeycombio/libhoney-go v1.15.8/go.mod h1:+tnL2etFnJmVx30yqmoUkVyQjp7
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
|
||||
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA=
|
||||
github.com/iancoleman/orderedmap v0.2.0 h1:sq1N/TFpYH++aViPcaKjys3bDClUEU7s5B+z6jq8pNA=
|
||||
github.com/iancoleman/orderedmap v0.2.0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA=
|
||||
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
|
||||
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
@ -1081,8 +1078,8 @@ github.com/inconshreveable/log15 v0.0.0-20201112154412-8562bdadbbac/go.mod h1:cO
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/invopop/jsonschema v0.7.0 h1:2vgQcBz1n256N+FpX3Jq7Y17AjYt46Ig3zIWyy770So=
|
||||
github.com/invopop/jsonschema v0.7.0/go.mod h1:O9uiLokuu0+MGFlyiaqtWxwqJm41/+8Nj0lD7A36YH0=
|
||||
github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI=
|
||||
github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
|
||||
github.com/itchyny/gojq v0.12.11 h1:YhLueoHhHiN4mkfM+3AyJV6EPcCxKZsOnYf+aVSwaQw=
|
||||
github.com/itchyny/gojq v0.12.11/go.mod h1:o3FT8Gkbg/geT4pLI0tF3hvip5F3Y/uskjRz9OYa38g=
|
||||
github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE=
|
||||
@ -1701,7 +1698,6 @@ github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
@ -1762,8 +1758,8 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||
github.com/vvakame/gcplogurl v0.2.0 h1:dH55ru2OQOIAKjZi5wwXjNnSfN0oXLFYkMQy908s+tU=
|
||||
github.com/vvakame/gcplogurl v0.2.0/go.mod h1:CFjKFlur6M+/2DoGZL67O1FqZxB42jiqCyl4cXAmjOU=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.5 h1:jLbYIFyWQMUwHLO20cImlCRBoNc5lp0nmE2dvwcxc7k=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.5/go.mod h1:9Xvgm2mV2kSq2SAm0Y608tBmu8akTzI7c2bz7/G7ZN4=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0/go.mod h1:IXCdmsXIht47RaVFLEdVnh1t+pgYtTAhQGj73kz+2DM=
|
||||
github.com/xanzy/go-gitlab v0.86.0 h1:jR8V9cK9jXRQDb46KOB20NCF3ksY09luaG0IfXE6p7w=
|
||||
github.com/xanzy/go-gitlab v0.86.0/go.mod h1:5ryv+MnpZStBH8I/77HuQBsMbBGANtVpLWC15qOjWAw=
|
||||
|
||||
Loading…
Reference in New Issue
Block a user