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
}