diff --git a/cmd/argo-actions/README.md b/cmd/argo-actions/README.md index 9efc6f218..2a64bf578 100644 --- a/cmd/argo-actions/README.md +++ b/cmd/argo-actions/README.md @@ -35,6 +35,7 @@ The following environment variables can be set: |--------------------------|----------|-------------------------------------------------|--------------------------------------------------------| | APP_ACTION | yes | | Defines action to perform | | APP_LOCAL_HUB_ENDPOINT | no | http://capact-hub-local.capact-system/graphql | Defines local Hub Endpoint | +| APP_PUBLIC_HUB_ENDPOINT | no | http://capact-hub-public.capact-system/graphql | Defines public Hub Endpoint | | APP_DOWNLOAD_CONFIG | no | | For download action defines Type Instances to download | | APP_LOGGER_DEV_MODE | no | `false` | Enable additional log messages | diff --git a/cmd/argo-actions/main.go b/cmd/argo-actions/main.go index c441e44a8..bf0eab4bf 100644 --- a/cmd/argo-actions/main.go +++ b/cmd/argo-actions/main.go @@ -40,19 +40,16 @@ func main() { logger, err := logger.New(cfg.Logger) exitOnError(err, "while creating zap logger") - localClient := local.NewDefaultClient(cfg.LocalHubEndpoint) - publicClient := public.NewDefaultClient(cfg.PublicHubEndpoint) - // TODO: Consider using connection `hubclient.New` and route requests through Gateway client := hubclient.Client{ - Local: localClient, - Public: publicClient, + Local: local.NewDefaultClient(cfg.LocalHubEndpoint), + Public: public.NewDefaultClient(cfg.PublicHubEndpoint), } switch cfg.Action { case argoactions.DownloadAction: log := logger.With(zap.String("Action", argoactions.DownloadAction)) - action = argoactions.NewDownloadAction(log, localClient, cfg.DownloadConfig) + action = argoactions.NewDownloadAction(log, &client, cfg.DownloadConfig) case argoactions.UploadAction: log := logger.With(zap.String("Action", argoactions.UploadAction)) diff --git a/cmd/k8s-engine/main.go b/cmd/k8s-engine/main.go index a4fc02591..2c0deedb2 100644 --- a/cmd/k8s-engine/main.go +++ b/cmd/k8s-engine/main.go @@ -63,6 +63,9 @@ type Config struct { Logger logger.Config + LocalHubEndpoint string `envconfig:"default=http://capact-hub-local.capact-system/graphql"` + PublicHubEndpoint string `envconfig:"default=http://capact-hub-public.capact-system/graphql"` + GraphQLGateway struct { Endpoint string `envconfig:"default=http://capact-gateway/graphql"` Username string @@ -107,7 +110,7 @@ func main() { exitOnError(err, "while creating manager") hubClient := getHubClient(&cfg) - typeInstanceHandler := argo.NewTypeInstanceHandler(cfg.HubActionsImage) + typeInstanceHandler := argo.NewTypeInstanceHandler(cfg.HubActionsImage, cfg.LocalHubEndpoint, cfg.PublicHubEndpoint) interfaceIOValidator := actionvalidation.NewValidator(hubClient) policyIOValidator := policyvalidation.NewValidator(hubClient) wfValidator := renderer.NewWorkflowInputValidator(interfaceIOValidator, policyIOValidator) diff --git a/deploy/kubernetes/charts/capact/charts/engine/templates/deployment.yaml b/deploy/kubernetes/charts/capact/charts/engine/templates/deployment.yaml index bf642f528..a2080c910 100644 --- a/deploy/kubernetes/charts/capact/charts/engine/templates/deployment.yaml +++ b/deploy/kubernetes/charts/capact/charts/engine/templates/deployment.yaml @@ -48,6 +48,10 @@ spec: value: "{{ .Values.global.gateway.auth.username }}" - name: APP_GRAPHQLGATEWAY_PASSWORD value: "{{ .Values.global.gateway.auth.password }}" + - name: APP_LOCAL_HUB_ENDPOINT + value: "http://capact-hub-local.{{.Release.Namespace}}.svc.cluster.local/graphql" + - name: APP_PUBLIC_HUB_ENDPOINT + value: "http://capact-hub-public.{{.Release.Namespace}}.svc.cluster.local/graphql" - name: APP_BUILTIN_RUNNER_IMAGE value: "{{ .Values.global.containerRegistry.path }}/{{ .Values.builtInRunner.image.name }}:{{ .Values.global.containerRegistry.overrideTag | default .Chart.AppVersion }}" - name: APP_BUILTIN_RUNNER_TIMEOUT diff --git a/pkg/argo-actions/download_type_instances.go b/pkg/argo-actions/download_type_instances.go index 947e97844..7ef5c0c7b 100644 --- a/pkg/argo-actions/download_type_instances.go +++ b/pkg/argo-actions/download_type_instances.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "capact.io/capact/pkg/hub/client/local" + hubclient "capact.io/capact/pkg/hub/client" "capact.io/capact/pkg/runner" "github.com/pkg/errors" "go.uber.org/zap" @@ -25,11 +25,11 @@ type DownloadConfig struct { type Download struct { log *zap.Logger cfg []DownloadConfig - client *local.Client + client *hubclient.Client } // NewDownloadAction returns a new Download instance. -func NewDownloadAction(log *zap.Logger, client *local.Client, cfg []DownloadConfig) Action { +func NewDownloadAction(log *zap.Logger, client *hubclient.Client, cfg []DownloadConfig) Action { return &Download{ log: log, cfg: cfg, diff --git a/pkg/sdk/apis/0.0.1/types/types.extend.go b/pkg/sdk/apis/0.0.1/types/types.extend.go index cabcabc5d..06c1cf1d1 100644 --- a/pkg/sdk/apis/0.0.1/types/types.extend.go +++ b/pkg/sdk/apis/0.0.1/types/types.extend.go @@ -1,6 +1,10 @@ // Package types holds manually added types. package types +import ( + "fmt" +) + // OCFPathPrefix defines path prefix that all OCF manifest must have. const OCFPathPrefix = "cap." @@ -20,6 +24,10 @@ type ManifestRef struct { Revision string `json:"revision"` // Version of the manifest content in the SemVer format. } +func (in *ManifestRef) String() string { + return fmt.Sprintf("%s:%s", in.Path, in.Revision) +} + // ManifestRefWithOptRevision specifies type by path and optional revision. // +kubebuilder:object:generate=true type ManifestRefWithOptRevision struct { diff --git a/pkg/sdk/renderer/argo/dedicated_renderer_test.go b/pkg/sdk/renderer/argo/dedicated_renderer_test.go index cc0278687..3f3f3298d 100644 --- a/pkg/sdk/renderer/argo/dedicated_renderer_test.go +++ b/pkg/sdk/renderer/argo/dedicated_renderer_test.go @@ -14,7 +14,7 @@ func createFakeDedicatedRendererObject(t *testing.T) *dedicatedRenderer { require.NoError(t, err) genUUID := func() string { return "uuid" } - typeInstanceHandler := NewTypeInstanceHandler("alpine:3.7") + typeInstanceHandler := NewTypeInstanceHandler("alpine:3.7", "", "") typeInstanceHandler.SetGenUUID(genUUID) policyIOValidator := policyvalidation.NewValidator(fakeCli) diff --git a/pkg/sdk/renderer/argo/renderer_test.go b/pkg/sdk/renderer/argo/renderer_test.go index 7ec1a71f7..5759944a1 100644 --- a/pkg/sdk/renderer/argo/renderer_test.go +++ b/pkg/sdk/renderer/argo/renderer_test.go @@ -36,7 +36,7 @@ func TestRenderHappyPath(t *testing.T) { policy := policy.NewAllowAll() genUUID := func() string { return "uuid" } // it has to be static because of parallel testing - typeInstanceHandler := NewTypeInstanceHandler("alpine:3.7") + typeInstanceHandler := NewTypeInstanceHandler("alpine:3.7", "", "") typeInstanceHandler.SetGenUUID(genUUID) ownerID := "default/action" @@ -294,7 +294,7 @@ func TestRenderHappyPathWithCustomPolicies(t *testing.T) { tt := test t.Run(tt.name, func(t *testing.T) { genUUID := genUUIDFn(strconv.Itoa(tc)) - typeInstanceHandler := NewTypeInstanceHandler("alpine:3.7") + typeInstanceHandler := NewTypeInstanceHandler("alpine:3.7", "", "") typeInstanceHandler.SetGenUUID(genUUID) interfaceIOValidator := actionvalidation.NewValidator(fakeCli) @@ -341,7 +341,7 @@ func TestRendererMaxDepth(t *testing.T) { require.NoError(t, err) policy := policy.NewAllowAll() - typeInstanceHandler := NewTypeInstanceHandler("alpine:3.7") + typeInstanceHandler := NewTypeInstanceHandler("alpine:3.7", "", "") typeInstanceHandler.SetGenUUID(genUUIDFn("")) interfaceIOValidator := actionvalidation.NewValidator(fakeCli) @@ -380,7 +380,7 @@ func TestRendererDenyAllPolicy(t *testing.T) { require.NoError(t, err) policy := policy.NewDenyAll() - typeInstanceHandler := NewTypeInstanceHandler("alpine:3.7") + typeInstanceHandler := NewTypeInstanceHandler("alpine:3.7", "", "") typeInstanceHandler.SetGenUUID(genUUIDFn("")) interfaceIOValidator := actionvalidation.NewValidator(fakeCli) diff --git a/pkg/sdk/renderer/argo/typeinstance_handler.go b/pkg/sdk/renderer/argo/typeinstance_handler.go index 43644d8ad..1bda46ed1 100644 --- a/pkg/sdk/renderer/argo/typeinstance_handler.go +++ b/pkg/sdk/renderer/argo/typeinstance_handler.go @@ -17,14 +17,18 @@ import ( // TypeInstanceHandler provides functionality to handle TypeInstance operations such as // injecting download step and upload step. type TypeInstanceHandler struct { - hubActionsImage string - genUUID func() string + hubActionsImage string + localHubEndpoint string + publicHubEndpoint string + genUUID func() string } // NewTypeInstanceHandler returns a new TypeInstanceHandler instance. -func NewTypeInstanceHandler(hubActionsImage string) *TypeInstanceHandler { +func NewTypeInstanceHandler(hubActionsImage string, localHubEndpoint string, publicHubEndpoint string) *TypeInstanceHandler { return &TypeInstanceHandler{ - hubActionsImage: hubActionsImage, + hubActionsImage: hubActionsImage, + localHubEndpoint: localHubEndpoint, + publicHubEndpoint: publicHubEndpoint, genUUID: func() string { return uuid.New().String() }, @@ -70,6 +74,14 @@ func (r *TypeInstanceHandler) AddInputTypeInstances(rootWorkflow *Workflow, inst Name: "APP_DOWNLOAD_CONFIG", Value: strings.Join(typeInstanceToDownload, ","), }, + { + Name: "APP_LOCAL_HUB_ENDPOINT", + Value: r.localHubEndpoint, + }, + { + Name: "APP_PUBLIC_HUB_ENDPOINT", + Value: r.publicHubEndpoint, + }, }, }, Outputs: wfv1.Outputs{ @@ -197,6 +209,14 @@ func (r *TypeInstanceHandler) AddUploadTypeInstancesStep(rootWorkflow *Workflow, Name: "APP_UPLOAD_CONFIG_TYPE_INSTANCES_DIR", Value: "/upload/typeInstances", }, + { + Name: "APP_LOCAL_HUB_ENDPOINT", + Value: r.localHubEndpoint, + }, + { + Name: "APP_PUBLIC_HUB_ENDPOINT", + Value: r.publicHubEndpoint, + }, }, }, Inputs: wfv1.Inputs{ @@ -288,6 +308,14 @@ func (r *TypeInstanceHandler) AddUpdateTypeInstancesStep(rootWorkflow *Workflow, Name: "APP_UPDATE_CONFIG_TYPE_INSTANCES_DIR", Value: "/update/typeInstances", }, + { + Name: "APP_LOCAL_HUB_ENDPOINT", + Value: r.localHubEndpoint, + }, + { + Name: "APP_PUBLIC_HUB_ENDPOINT", + Value: r.publicHubEndpoint, + }, }, }, Inputs: wfv1.Inputs{ diff --git a/pkg/sdk/validation/typeinstance.go b/pkg/sdk/validation/typeinstance.go index ffadafa65..ead9ab917 100644 --- a/pkg/sdk/validation/typeinstance.go +++ b/pkg/sdk/validation/typeinstance.go @@ -4,18 +4,34 @@ import ( "context" "encoding/json" "fmt" + "strings" + "capact.io/capact/internal/ptr" graphqllocal "capact.io/capact/pkg/hub/api/graphql/local" "capact.io/capact/pkg/sdk/apis/0.0.1/types" "github.com/pkg/errors" "github.com/xeipuuv/gojsonschema" ) -type typeInstanceData struct { - alias *string - id string - value interface{} - typeRefWithRevision string +// TypeInstanceEssentialData contains essential TypeInstance Data for validation purpose. +type TypeInstanceEssentialData struct { + Value interface{} + TypeRef types.ManifestRef + Alias *string + ID *string +} + +func (ti *TypeInstanceEssentialData) String() string { + out := &strings.Builder{} + fmt.Fprintf(out, "ID: %s,Alias: %s", ti.stringPtrToString(ti.ID), ti.stringPtrToString(ti.Alias)) + return out.String() +} + +func (ti *TypeInstanceEssentialData) stringPtrToString(p *string) string { + if p != nil { + return *p + } + return "" } // TypeInstanceValidationHubClient defines Hub methods needed for validation of TypeInstances. @@ -26,25 +42,24 @@ type TypeInstanceValidationHubClient interface { // ValidateTypeInstancesToCreate is responsible for validating TypeInstance which do not exist and will be created. func ValidateTypeInstancesToCreate(ctx context.Context, client TypeInstanceValidationHubClient, typeInstance *graphqllocal.CreateTypeInstancesInput) (Result, error) { - var typeInstanceCollection []*typeInstanceData + var typeInstanceCollection []*TypeInstanceEssentialData typeRefCollection := TypeRefCollection{} for _, ti := range typeInstance.TypeInstances { - if ti == nil { + if ti == nil || ti.TypeRef == nil { continue } - typeRef := types.TypeRef{ + manifestRef := types.ManifestRef{ Path: ti.TypeRef.Path, Revision: ti.TypeRef.Revision, } - name := getManifestPathWithRevision(ti.TypeRef.Path, ti.TypeRef.Revision) - typeRefCollection[name] = TypeRef{ - TypeRef: typeRef, + typeRefCollection[manifestRef.String()] = TypeRef{ + TypeRef: types.TypeRef(manifestRef), } - typeInstanceCollection = append(typeInstanceCollection, &typeInstanceData{ - alias: ti.Alias, - value: ti.Value, - typeRefWithRevision: name, + typeInstanceCollection = append(typeInstanceCollection, &TypeInstanceEssentialData{ + Alias: ti.Alias, + Value: ti.Value, + TypeRef: manifestRef, }) } @@ -52,13 +67,12 @@ func ValidateTypeInstancesToCreate(ctx context.Context, client TypeInstanceValid if err != nil { return nil, errors.Wrapf(err, "while resolving TypeRefs to JSON Schemas") } - return validateTypeInstances(schemasCollection, typeInstanceCollection) + return ValidateTypeInstances(schemasCollection, typeInstanceCollection) } -// ValidateTypeInstanceToUpdate is responsible for validating TypeInstance which exists and will be updated. +// ValidateTypeInstanceToUpdate is responsible for validating TypeInstance which exists and will be updated. func ValidateTypeInstanceToUpdate(ctx context.Context, client TypeInstanceValidationHubClient, typeInstanceToUpdate []graphqllocal.UpdateTypeInstancesInput) (Result, error) { var typeInstanceIds []string - idToTypeNameMap := map[string]string{} for _, ti := range typeInstanceToUpdate { typeInstanceIds = append(typeInstanceIds, ti.ID) } @@ -69,26 +83,29 @@ func ValidateTypeInstanceToUpdate(ctx context.Context, client TypeInstanceValida } typeRefCollection := TypeRefCollection{} - for id, typeReference := range typeInstancesTypeRef { - name := getManifestPathWithRevision(typeReference.Path, typeReference.Revision) - typeRefCollection[name] = TypeRef{ - TypeRef: types.TypeRef{ - Path: typeReference.Path, - Revision: typeReference.Revision, - }, + for _, typeReference := range typeInstancesTypeRef { + manifestRef := types.ManifestRef{ + Path: typeReference.Path, + Revision: typeReference.Revision, + } + typeRefCollection[manifestRef.String()] = TypeRef{ + TypeRef: types.TypeRef(manifestRef), } - idToTypeNameMap[id] = name } - var typeInstanceCollection []*typeInstanceData + var typeInstanceCollection []*TypeInstanceEssentialData for _, ti := range typeInstanceToUpdate { if ti.TypeInstance == nil { continue } - typeInstanceCollection = append(typeInstanceCollection, &typeInstanceData{ - id: ti.ID, - value: ti.TypeInstance.Value, - typeRefWithRevision: idToTypeNameMap[ti.ID], + typeRef, ok := typeInstancesTypeRef[ti.ID] + if !ok { + return nil, errors.Wrapf(err, "while finding TypeInstance Type reference for id %s", ti.ID) + } + typeInstanceCollection = append(typeInstanceCollection, &TypeInstanceEssentialData{ + ID: ptr.String(ti.ID), + Value: ti.TypeInstance.Value, + TypeRef: types.ManifestRef(typeRef), }) } @@ -97,25 +114,26 @@ func ValidateTypeInstanceToUpdate(ctx context.Context, client TypeInstanceValida return nil, errors.Wrapf(err, "while resolving TypeRefs to JSON Schemas") } - return validateTypeInstances(schemasCollection, typeInstanceCollection) + return ValidateTypeInstances(schemasCollection, typeInstanceCollection) } -func validateTypeInstances(schemaCollection SchemaCollection, typeInstanceCollection []*typeInstanceData) (Result, error) { +//ValidateTypeInstances is responsible for validating TypeInstance. +func ValidateTypeInstances(schemaCollection SchemaCollection, typeInstanceCollection []*TypeInstanceEssentialData) (Result, error) { resultBldr := NewResultBuilder("Validation TypeInstances") for _, ti := range typeInstanceCollection { - if _, ok := ti.value.(map[string]interface{}); !ok { + if _, ok := ti.Value.(map[string]interface{}); !ok { return Result{}, errors.New("could not create map from TypeInstance Value") } - valuesJSON, err := json.Marshal(ti.value) + valuesJSON, err := json.Marshal(ti.Value) if err != nil { return Result{}, errors.Wrap(err, "while converting TypeInstance value to JSON bytes") } - if _, ok := schemaCollection[ti.typeRefWithRevision]; !ok { - return Result{}, fmt.Errorf("could not find Schema for type %s", ti.typeRefWithRevision) + if _, ok := schemaCollection[ti.TypeRef.String()]; !ok { + return Result{}, fmt.Errorf("could not find Schema for type %s", ti.TypeRef.String()) } - schemaLoader := gojsonschema.NewStringLoader(schemaCollection[ti.typeRefWithRevision].Value) + schemaLoader := gojsonschema.NewStringLoader(schemaCollection[ti.TypeRef.String()].Value) dataLoader := gojsonschema.NewBytesLoader(valuesJSON) result, err := gojsonschema.Validate(schemaLoader, dataLoader) @@ -124,12 +142,7 @@ func validateTypeInstances(schemaCollection SchemaCollection, typeInstanceCollec } if !result.Valid() { for _, err := range result.Errors() { - msg := "" - if ti.alias != nil { - msg = fmt.Sprintf("TypeInstance with alias %s", *ti.alias) - } else if ti.id != "" { - msg = fmt.Sprintf("TypeInstance with id %s", ti.id) - } + msg := fmt.Sprintf("TypeInstance(%s)", ti.String()) resultBldr.ReportIssue(msg, err.String()) } } @@ -137,7 +150,3 @@ func validateTypeInstances(schemaCollection SchemaCollection, typeInstanceCollec return resultBldr.Result(), nil } - -func getManifestPathWithRevision(path string, revision string) string { - return path + ":" + revision -} diff --git a/pkg/sdk/validation/typeinstance_test.go b/pkg/sdk/validation/typeinstance_test.go index 084e3ff61..071ee0a1e 100644 --- a/pkg/sdk/validation/typeinstance_test.go +++ b/pkg/sdk/validation/typeinstance_test.go @@ -4,6 +4,8 @@ import ( "fmt" "testing" + "capact.io/capact/internal/ptr" + "capact.io/capact/pkg/sdk/apis/0.0.1/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -11,7 +13,7 @@ import ( func TestValidateTypeInstances(t *testing.T) { tests := map[string]struct { schemaCollection SchemaCollection - typeInstanceCollection []*typeInstanceData + typeInstanceCollection []*TypeInstanceEssentialData expError error }{ "When TypeInstance values do not contain the required property": { @@ -21,14 +23,17 @@ func TestValidateTypeInstances(t *testing.T) { Required: false, }, }, - typeInstanceCollection: []*typeInstanceData{ + typeInstanceCollection: []*TypeInstanceEssentialData{ { - typeRefWithRevision: "cap.type.aws.auth.creds:0.1.0", - value: map[string]interface{}{ + TypeRef: types.ManifestRef{ + Path: "cap.type.aws.auth.creds", + Revision: "0.1.0", + }, + Value: map[string]interface{}{ "test1": "test", "test2": "test", }, - alias: pointerToAlias("aws-creds"), + Alias: ptr.String("aws-creds"), }, }, expError: fmt.Errorf("%s", "- Validation TypeInstances \"TypeInstance with alias aws-creds\":\n * (root): key is required"), @@ -40,13 +45,16 @@ func TestValidateTypeInstances(t *testing.T) { Required: false, }, }, - typeInstanceCollection: []*typeInstanceData{ + typeInstanceCollection: []*TypeInstanceEssentialData{ { - typeRefWithRevision: "cap.type.aws.elasticsearch.install-input:0.1.0", - value: map[string]interface{}{ + TypeRef: types.ManifestRef{ + Path: "cap.type.aws.elasticsearch.install-input", + Revision: "0.1.0", + }, + Value: map[string]interface{}{ "replicas": 5, }, - id: "5605af48-c34f-4bdc-b2d8-53c679bdfa5a", + ID: ptr.String("5605af48-c34f-4bdc-b2d8-53c679bdfa5a"), }, }, expError: fmt.Errorf("%s", "- Validation TypeInstances \"TypeInstance with id 5605af48-c34f-4bdc-b2d8-53c679bdfa5a\":\n * replicas: Invalid type. Expected: string, given: integer"), @@ -58,22 +66,56 @@ func TestValidateTypeInstances(t *testing.T) { Required: false, }, }, - typeInstanceCollection: []*typeInstanceData{ + typeInstanceCollection: []*TypeInstanceEssentialData{ { - typeRefWithRevision: "cap.type.aws.auth.creds:0.1.0", - value: map[string]interface{}{ + TypeRef: types.ManifestRef{ + Path: "cap.type.aws.auth.creds", + Revision: "0.1.0", + }, + Value: map[string]interface{}{ "key": "aaa", }, }, }, expError: nil, }, + "When there is a collection of TypeInstance with an incorrect value": { + schemaCollection: SchemaCollection{ + "cap.type.aws.auth.creds:0.1.0": { + Value: fmt.Sprintf("%v", AWSCredsTypeRevFixture().Revisions[0].Spec.JSONSchema), + Required: false, + }, + }, + typeInstanceCollection: []*TypeInstanceEssentialData{ + { + TypeRef: types.ManifestRef{ + Path: "cap.type.aws.auth.creds", + Revision: "0.1.0", + }, + Value: map[string]interface{}{ + "test1": "test", + }, + Alias: ptr.String("aws-creds"), + }, + { + TypeRef: types.ManifestRef{ + Path: "cap.type.aws.auth.creds", + Revision: "0.1.0", + }, + Value: map[string]interface{}{ + "test2": "test", + }, + Alias: ptr.String("aws-creds-2"), + }, + }, + expError: fmt.Errorf("%s", "- Validation TypeInstances \"TypeInstance with alias aws-creds\":\n * (root): key is required\n- Validation TypeInstances \"TypeInstance with alias aws-creds-2\":\n * (root): key is required"), + }, } for tn, tc := range tests { t.Run(tn, func(t *testing.T) { // when - validationResults, err := validateTypeInstances(tc.schemaCollection, tc.typeInstanceCollection) + validationResults, err := ValidateTypeInstances(tc.schemaCollection, tc.typeInstanceCollection) // then require.NoError(t, err) @@ -81,7 +123,3 @@ func TestValidateTypeInstances(t *testing.T) { }) } } - -func pointerToAlias(alias string) *string { - return &alias -}