internal/provider/sdk/util.go (314 lines of code) (raw):

package sdk import ( "context" "fmt" "net/url" "regexp" "strings" "time" "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "gitlab.com/gitlab-org/api/client-go" "maps" "slices" ) var validateDateFunc = func(v any, k string) (we []string, errors []error) { value := v.(string) //add zero hours and let time figure out correctness _, e := time.Parse(time.RFC3339, value+"T00:00:00Z") if e != nil { errors = append(errors, fmt.Errorf("%s is not valid for format YYYY-MM-DD", value)) } return } var validateURLFunc = func(v any, k string) (s []string, errors []error) { value := v.(string) url, err := url.Parse(value) if err != nil || url.Host == "" || url.Scheme == "" { errors = append(errors, fmt.Errorf("%s is not a valid URL", value)) return } return } func stringToVisibilityLevel(s string) *gitlab.VisibilityValue { lookup := map[string]gitlab.VisibilityValue{ "private": gitlab.PrivateVisibility, "internal": gitlab.InternalVisibility, "public": gitlab.PublicVisibility, } value, ok := lookup[s] if !ok { return nil } return &value } func stringToProjectCreationLevel(s string) *gitlab.ProjectCreationLevelValue { lookup := map[string]gitlab.ProjectCreationLevelValue{ "owner": gitlab.OwnerProjectCreation, "noone": gitlab.NoOneProjectCreation, "maintainer": gitlab.MaintainerProjectCreation, "developer": gitlab.DeveloperProjectCreation, } value, ok := lookup[s] if !ok { return nil } return &value } func stringToSharedRunnersSetting(s string) *gitlab.SharedRunnersSettingValue { lookup := map[string]gitlab.SharedRunnersSettingValue{ "enabled": gitlab.EnabledSharedRunnersSettingValue, "disabled_and_overridable": gitlab.DisabledAndOverridableSharedRunnersSettingValue, "disabled_and_unoverridable": gitlab.DisabledAndUnoverridableSharedRunnersSettingValue, // nolint:staticcheck // SA1019 ignore deprecated DisabledWithOverrideSharedRunnersSettingValue "disabled_with_override": gitlab.DisabledWithOverrideSharedRunnersSettingValue, } value, ok := lookup[s] if !ok { return nil } return &value } func stringToSubGroupCreationLevel(s string) *gitlab.SubGroupCreationLevelValue { lookup := map[string]gitlab.SubGroupCreationLevelValue{ "owner": gitlab.OwnerSubGroupCreationLevelValue, "maintainer": gitlab.MaintainerSubGroupCreationLevelValue, } value, ok := lookup[s] if !ok { return nil } return &value } func stringToVariableType(s string) *gitlab.VariableTypeValue { lookup := map[string]gitlab.VariableTypeValue{ "env_var": gitlab.EnvVariableType, "file": gitlab.FileVariableType, } value, ok := lookup[s] if !ok { return nil } return &value } func stringToMergeMethod(s string) *gitlab.MergeMethodValue { lookup := map[string]gitlab.MergeMethodValue{ "merge": gitlab.NoFastForwardMerge, "ff": gitlab.FastForwardMerge, "rebase_merge": gitlab.RebaseMerge, } value, ok := lookup[s] if !ok { return nil } return &value } func stringToSquashOptionValue(s string) *gitlab.SquashOptionValue { lookup := map[string]gitlab.SquashOptionValue{ "never": gitlab.SquashOptionNever, "always": gitlab.SquashOptionAlways, "default_on": gitlab.SquashOptionDefaultOn, "default_off": gitlab.SquashOptionDefaultOff, } value, ok := lookup[s] if !ok { return nil } return &value } func stringToAccessControlValue(s string) *gitlab.AccessControlValue { lookup := map[string]gitlab.AccessControlValue{ "disabled": gitlab.DisabledAccessControl, "enabled": gitlab.EnabledAccessControl, "private": gitlab.PrivateAccessControl, "public": gitlab.PublicAccessControl, } value, ok := lookup[s] if !ok { return nil } return &value } // lintignore: V011 // TODO: Resolve this tfproviderlint issue var StringIsGitlabVariableName = func(v any, k string) (s []string, es []error) { value, ok := v.(string) if !ok { es = append(es, fmt.Errorf("expected type of %s to be string", k)) return } if len(value) < 1 || len(value) > 255 { es = append(es, fmt.Errorf("expected length of %s to be in the range (%d - %d), got %s", k, 1, 255, v)) } match, _ := regexp.MatchString("[a-zA-Z0-9_]+", value) if !match { es = append(es, fmt.Errorf("%s is an invalid value for argument %s. Only A-Z, a-z, 0-9, and _ are allowed", value, k)) } return } var StringIsGitlabVariableType = func(v any, k string) (s []string, es []error) { value, ok := v.(string) if !ok { es = append(es, fmt.Errorf("expected type of %s to be string", k)) return } variableType := stringToVariableType(value) if variableType == nil { es = append(es, fmt.Errorf("expected variable_type to be \"env_var\" or \"file\"")) } return } func stringListToStringSlice(stringList []any) *[]string { ret := []string{} if stringList == nil { return &ret } for _, v := range stringList { ret = append(ret, fmt.Sprint(v)) } return &ret } func stringSetToStringSlice(stringSet *schema.Set) *[]string { ret := []string{} if stringSet == nil { return &ret } for _, envVal := range stringSet.List() { ret = append(ret, envVal.(string)) } return &ret } func intSetToIntSlice(intSet *schema.Set) *[]int { ret := []int{} if intSet == nil { return &ret } for _, envVal := range intSet.List() { ret = append(ret, envVal.(int)) } return &ret } func intListToIntSlice(intList []any) *[]int { ret := []int{} if intList == nil { return &ret } for _, envVal := range intList { ret = append(ret, envVal.(int)) } return &ret } func stringListToVisibilityLevelSlice(strings []any) *[]gitlab.VisibilityValue { ret := []gitlab.VisibilityValue{} if strings == nil { return &ret } for _, envVal := range strings { ret = append(ret, *stringToVisibilityLevel(envVal.(string))) } return &ret } func stringListToCommaSeparatedString(stringList []any) *string { ret := strings.Join(*stringListToStringSlice(stringList), ",") return &ret } func fromIntegerMap(value any) map[string]int { integerMap := make(map[string]int) for k, v := range value.(map[string]any) { integerMap[k] = v.(int) } return integerMap } // ISO 8601 date format const iso8601 = "2006-01-02" // isISO8601 validates if the given value is a ISO8601 compatible date in the YYYY-MM-DD format. func isISO6801Date(i any, p cty.Path) diag.Diagnostics { v := i.(string) if _, err := time.Parse(iso8601, v); err != nil { return diag.Errorf("expected %q to be a valid YYYY-MM-DD date, got %q: %+v", p, i, err) } return nil } func parseISO8601Date(v string) (*gitlab.ISOTime, error) { iso8601Date, err := time.Parse(iso8601, v) if err != nil { return nil, fmt.Errorf("expected %q to be a valid YYYY-MM-DD date", v) } x := gitlab.ISOTime(iso8601Date) return &x, nil } // contains checks if a string is present in a slice func contains(s []string, str string) bool { return slices.Contains(s, str) } func constructSchema(schemas ...map[string]*schema.Schema) map[string]*schema.Schema { schema := make(map[string]*schema.Schema) for _, s := range schemas { maps.Copy(schema, s) } return schema } // datasourceSchemaFromResourceSchema is a recursive func that // converts an existing Resource schema to a Datasource schema. // All schema elements are copied, but certain attributes are ignored or changed: // - all attributes have Computed = true // - all attributes have ForceNew, Required = false // - Validation funcs and attributes (e.g. MaxItems) are not copied // Adapted from https://github.com/hashicorp/terraform-provider-google/blob/1a72f93a8dcf6f1e59d5f25aefcb6d794a116bf5/google/datasource_helpers.go#L13 func datasourceSchemaFromResourceSchema(rs map[string]*schema.Schema, arguments []string, optionalArguments []string, excludedElements ...string) map[string]*schema.Schema { ds := make(map[string]*schema.Schema, len(rs)) for k, v := range rs { dv := &schema.Schema{ ForceNew: false, Description: v.Description, Type: v.Type, } if contains(arguments, k) { dv.Computed = false dv.Required = true } else { dv.Computed = true dv.Required = false } if contains(optionalArguments, k) { dv.Optional = true } switch v.Type { case schema.TypeSet: dv.Set = v.Set fallthrough case schema.TypeList: // List & Set types are generally used for 2 cases: // - a list/set of simple primitive values (e.g. list of strings) // - a sub resource if elem, ok := v.Elem.(*schema.Resource); ok { // handle the case where the Element is a sub-resource dv.Elem = &schema.Resource{ Schema: datasourceSchemaFromResourceSchema(elem.Schema, nil, nil), } } else { // handle simple primitive case dv.Elem = v.Elem } default: // Elem of all other types are copied as-is dv.Elem = v.Elem } ds[k] = dv } datasourceSchema := excludeElementsFromSchema(ds, excludedElements) return datasourceSchema } func excludeElementsFromSchema(oldSchema map[string]*schema.Schema, excludedElements []string) map[string]*schema.Schema { newSchema := make(map[string]*schema.Schema, len(oldSchema)) for k, v := range oldSchema { if !contains(excludedElements, k) { newSchema[k] = v } } return newSchema } func setStateMapInResourceData(stateMap map[string]any, d *schema.ResourceData) error { for k, v := range stateMap { // lintignore: R001 // for convenience sake, to reduce maintenance burden we are ok not having literals here. if err := d.Set(k, v); err != nil { return fmt.Errorf("failed to set state for %q to %v: %w", k, v, err) } } return nil } // lock can be used to lock, but make it `context.Context` aware. // e.g. it'll respect cancelling and timeouts. type lock chan struct{} func newLock() lock { return make(lock, 1) } func (c lock) lock(ctx context.Context) error { select { case c <- struct{}{}: // lock acquired return nil case <-ctx.Done(): // Timeout return ctx.Err() } } func (c lock) unlock() { <-c }