blob: 55a9f50535a35a3bdadf0fad942bb665358a1e14 [file] [log] [blame] [edit]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package tpgresource
import (
"context"
"crypto/md5"
"encoding/base64"
"errors"
"fmt"
"io/ioutil"
"log"
"net/url"
"reflect"
"regexp"
"sort"
"strconv"
"strings"
"time"
transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/go-cty/cty"
fwDiags "github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/id"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
"golang.org/x/exp/maps"
"google.golang.org/api/googleapi"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type TerraformResourceDataChange interface {
GetChange(string) (interface{}, interface{})
}
type TerraformResourceData interface {
HasChange(string) bool
GetOkExists(string) (interface{}, bool)
GetOk(string) (interface{}, bool)
Get(string) interface{}
Set(string, interface{}) error
SetId(string)
Id() string
GetProviderMeta(interface{}) error
Timeout(key string) time.Duration
}
type TerraformResourceDiff interface {
HasChange(string) bool
GetChange(string) (interface{}, interface{})
Get(string) interface{}
GetOk(string) (interface{}, bool)
Clear(string) error
ForceNew(string) error
}
// Contains functions that don't really belong anywhere else.
// GetRegionFromZone returns the region from a zone for Google cloud.
// This is by removing the characters after the last '-'.
// e.g. southamerica-west1-a => southamerica-west1
func GetRegionFromZone(zone string) string {
zoneParts := strings.Split(zone, "-")
if len(zoneParts) < 3 {
return ""
}
return strings.Join(zoneParts[:len(zoneParts)-1], "-")
}
// Infers the region based on the following (in order of priority):
// - `region` field in resource schema
// - region extracted from the `zone` field in resource schema
// - provider-level region
// - region extracted from the provider-level zone
func GetRegion(d TerraformResourceData, config *transport_tpg.Config) (string, error) {
return GetRegionFromSchema("region", "zone", d, config)
}
// GetProject reads the "project" field from the given resource data and falls
// back to the provider's value if not given. If the provider's value is not
// given, an error is returned.
func GetProject(d TerraformResourceData, config *transport_tpg.Config) (string, error) {
return GetProjectFromSchema("project", d, config)
}
// GetUniverse reads the "universe_domain" field from the given resource data and falls
// back to the provider's value if not given. If the provider's value is not
// given, an error is returned.
func GetUniverseDomain(d TerraformResourceData, config *transport_tpg.Config) (string, error) {
return GetUniverseDomainFromSchema("universe_domain", d, config)
}
// GetBillingProject reads the "billing_project" field from the given resource data and falls
// back to the provider's value if not given. If no value is found, an error is returned.
func GetBillingProject(d TerraformResourceData, config *transport_tpg.Config) (string, error) {
return GetBillingProjectFromSchema("billing_project", d, config)
}
// GetProjectFromDiff reads the "project" field from the given diff and falls
// back to the provider's value if not given. If the provider's value is not
// given, an error is returned.
func GetProjectFromDiff(d *schema.ResourceDiff, config *transport_tpg.Config) (string, error) {
res, ok := d.GetOk("project")
if ok {
return res.(string), nil
}
if d.GetRawConfig().GetAttr("project") == cty.UnknownVal(cty.String) {
return res.(string), nil
}
if config.Project != "" {
return config.Project, nil
}
return "", fmt.Errorf("%s: required field is not set", "project")
}
// getRegionFromDiff reads the "region" field from the given diff and falls
// back to the provider's value if not given. If the provider's value is not
// given, an error is returned.
func GetRegionFromDiff(d *schema.ResourceDiff, config *transport_tpg.Config) (string, error) {
res, ok := d.GetOk("region")
if ok {
return res.(string), nil
}
if d.GetRawConfig().GetAttr("region") == cty.UnknownVal(cty.String) {
return res.(string), nil
}
if config.Region != "" {
return config.Region, nil
}
return "", fmt.Errorf("%s: required field is not set", "region")
}
// getZoneFromDiff reads the "zone" field from the given diff and falls
// back to the provider's value if not given. If the provider's value is not
// given, an error is returned.
func GetZoneFromDiff(d *schema.ResourceDiff, config *transport_tpg.Config) (string, error) {
res, ok := d.GetOk("zone")
if ok {
return res.(string), nil
}
if d.GetRawConfig().GetAttr("zone") == cty.UnknownVal(cty.String) {
return res.(string), nil
}
if config.Zone != "" {
return config.Zone, nil
}
return "", fmt.Errorf("%s: required field is not set", "zone")
}
func GetRouterLockName(region string, router string) string {
return fmt.Sprintf("router/%s/%s", region, router)
}
func IsFailedPreconditionError(err error) bool {
gerr, ok := errwrap.GetType(err, &googleapi.Error{}).(*googleapi.Error)
if !ok {
return false
}
if gerr == nil {
return false
}
if gerr.Code != 400 {
return false
}
for _, e := range gerr.Errors {
if e.Reason == "failedPrecondition" {
return true
}
}
return false
}
func IsQuotaError(err error) bool {
gerr, ok := errwrap.GetType(err, &googleapi.Error{}).(*googleapi.Error)
if !ok {
return false
}
if gerr == nil {
return false
}
if gerr.Code != 429 {
return false
}
return true
}
func IsConflictError(err error) bool {
if e, ok := err.(*googleapi.Error); ok && (e.Code == 409 || e.Code == 412) {
return true
} else if !ok && errwrap.ContainsType(err, &googleapi.Error{}) {
e := errwrap.GetType(err, &googleapi.Error{}).(*googleapi.Error)
if e.Code == 409 || e.Code == 412 {
return true
}
}
return false
}
// gRPC does not return errors of type *googleapi.Error. Instead the errors returned are *status.Error.
// See the types of codes returned here (https://pkg.go.dev/google.golang.org/grpc/codes#Code).
func IsNotFoundGrpcError(err error) bool {
if errorStatus, ok := status.FromError(err); ok && errorStatus.Code() == codes.NotFound {
return true
}
return false
}
// ExpandLabels pulls the value of "labels" out of a TerraformResourceData as a map[string]string.
func ExpandLabels(d TerraformResourceData) map[string]string {
return ExpandStringMap(d, "labels")
}
// ExpandEffectiveLabels pulls the value of "effective_labels" out of a TerraformResourceData as a map[string]string.
func ExpandEffectiveLabels(d TerraformResourceData) map[string]string {
return ExpandStringMap(d, "effective_labels")
}
// ExpandEnvironmentVariables pulls the value of "environment_variables" out of a schema.ResourceData as a map[string]string.
func ExpandEnvironmentVariables(d *schema.ResourceData) map[string]string {
return ExpandStringMap(d, "environment_variables")
}
// ExpandBuildEnvironmentVariables pulls the value of "build_environment_variables" out of a schema.ResourceData as a map[string]string.
func ExpandBuildEnvironmentVariables(d *schema.ResourceData) map[string]string {
return ExpandStringMap(d, "build_environment_variables")
}
// ExpandStringMap pulls the value of key out of a TerraformResourceData as a map[string]string.
func ExpandStringMap(d TerraformResourceData, key string) map[string]string {
v, ok := d.GetOk(key)
if !ok {
return map[string]string{}
}
return ConvertStringMap(v.(map[string]interface{}))
}
// SortStringsByConfigOrder takes a slice of map[string]interface{} from a TF config
// and API data, and returns a new slice containing the API data, reorderd to match
// the TF config as closely as possible (with new items at the end of the list.)
func SortStringsByConfigOrder(configData, apiData []string) ([]string, error) {
configOrder := map[string]int{}
for index, item := range configData {
_, ok := configOrder[item]
if ok {
return nil, fmt.Errorf("configData element at %d has duplicate value `%s`", index, item)
}
configOrder[item] = index
}
apiSeen := map[string]struct{}{}
byConfigIndex := map[int]string{}
newElements := []string{}
for index, item := range apiData {
_, ok := apiSeen[item]
if ok {
return nil, fmt.Errorf("apiData element at %d has duplicate value `%s`", index, item)
}
apiSeen[item] = struct{}{}
configIndex, found := configOrder[item]
if found {
byConfigIndex[configIndex] = item
} else {
newElements = append(newElements, item)
}
}
// Sort set config indexes and convert to a slice of strings. This removes items present in the config
// but not present in the API response.
configIndexes := maps.Keys(byConfigIndex)
sort.Ints(configIndexes)
result := []string{}
for _, index := range configIndexes {
result = append(result, byConfigIndex[index])
}
// Add new elements to the end of the list, sorted alphabetically.
sort.Strings(newElements)
result = append(result, newElements...)
return result, nil
}
// SortMapsByConfigOrder takes a slice of map[string]interface{} from a TF config
// and API data, and returns a new slice containing the API data, reorderd to match
// the TF config as closely as possible (with new items at the end of the list.)
// idKey is be used to extract a string key from the values in the slice.
func SortMapsByConfigOrder(configData, apiData []map[string]interface{}, idKey string) ([]map[string]interface{}, error) {
configIds := make([]string, len(configData))
for i, item := range configData {
id, ok := item[idKey].(string)
if !ok {
return nil, fmt.Errorf("configData element at %d does not contain string value in key `%s`", i, idKey)
}
configIds[i] = id
}
apiIds := make([]string, len(apiData))
apiMap := map[string]map[string]interface{}{}
for i, item := range apiData {
id, ok := item[idKey].(string)
if !ok {
return nil, fmt.Errorf("apiData element at %d does not contain string value in key `%s`", i, idKey)
}
apiIds[i] = id
apiMap[id] = item
}
sortedIds, err := SortStringsByConfigOrder(configIds, apiIds)
if err != nil {
return nil, err
}
result := []map[string]interface{}{}
for _, id := range sortedIds {
result = append(result, apiMap[id])
}
return result, nil
}
func ConvertStringMap(v map[string]interface{}) map[string]string {
m := make(map[string]string)
for k, val := range v {
m[k] = val.(string)
}
return m
}
func ConvertStringArr(ifaceArr []interface{}) []string {
return ConvertAndMapStringArr(ifaceArr, func(s string) string { return s })
}
func ConvertAndMapStringArr(ifaceArr []interface{}, f func(string) string) []string {
var arr []string
for _, v := range ifaceArr {
if v == nil {
continue
}
arr = append(arr, f(v.(string)))
}
return arr
}
func MapStringArr(original []string, f func(string) string) []string {
var arr []string
for _, v := range original {
arr = append(arr, f(v))
}
return arr
}
func ConvertStringArrToInterface(strs []string) []interface{} {
arr := make([]interface{}, len(strs))
for i, str := range strs {
arr[i] = str
}
return arr
}
func ConvertStringSet(set *schema.Set) []string {
s := make([]string, 0, set.Len())
for _, v := range set.List() {
s = append(s, v.(string))
}
sort.Strings(s)
return s
}
func GolangSetFromStringSlice(strings []string) map[string]struct{} {
set := map[string]struct{}{}
for _, v := range strings {
set[v] = struct{}{}
}
return set
}
func StringSliceFromGolangSet(sset map[string]struct{}) []string {
ls := make([]string, 0, len(sset))
for s := range sset {
ls = append(ls, s)
}
sort.Strings(ls)
return ls
}
func ReverseStringMap(m map[string]string) map[string]string {
o := map[string]string{}
for k, v := range m {
o[v] = k
}
return o
}
func MergeStringMaps(a, b map[string]string) map[string]string {
merged := make(map[string]string)
for k, v := range a {
merged[k] = v
}
for k, v := range b {
merged[k] = v
}
return merged
}
func MergeSchemas(a, b map[string]*schema.Schema) map[string]*schema.Schema {
merged := make(map[string]*schema.Schema)
for k, v := range a {
merged[k] = v
}
for k, v := range b {
merged[k] = v
}
return merged
}
func StringToFixed64(v string) (int64, error) {
return strconv.ParseInt(v, 10, 64)
}
func ExtractFirstMapConfig(m []interface{}) map[string]interface{} {
if len(m) == 0 || m[0] == nil {
return map[string]interface{}{}
}
return m[0].(map[string]interface{})
}
// ServiceAccountFQN will attempt to generate the fully qualified name in the format of:
//
// "projects/(-|<project>)/serviceAccounts/<service_account_id>@<project>.iam.gserviceaccount.com"
// A project is required if we are trying to build the FQN from a service account id and
// and error will be returned in this case if no project is set in the resource or the
// provider-level config
func ServiceAccountFQN(serviceAccount string, d TerraformResourceData, config *transport_tpg.Config) (string, error) {
// If the service account id is already the fully qualified name
if strings.HasPrefix(serviceAccount, "projects/") {
return serviceAccount, nil
}
// If the service account id is an email
if strings.Contains(serviceAccount, "@") {
return "projects/-/serviceAccounts/" + serviceAccount, nil
}
// Get the project from the resource or fallback to the project
// in the provider configuration
project, err := GetProject(d, config)
if err != nil {
return "", err
}
return fmt.Sprintf("projects/-/serviceAccounts/%s@%s.iam.gserviceaccount.com", serviceAccount, project), nil
}
func PaginatedListRequest(project, baseUrl, userAgent string, config *transport_tpg.Config, flattener func(map[string]interface{}) []interface{}) ([]interface{}, error) {
res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{
Config: config,
Method: "GET",
Project: project,
RawURL: baseUrl,
UserAgent: userAgent,
})
if err != nil {
return nil, err
}
ls := flattener(res)
pageToken, ok := res["pageToken"]
for ok {
if pageToken.(string) == "" {
break
}
url := fmt.Sprintf("%s?pageToken=%s", baseUrl, pageToken.(string))
res, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{
Config: config,
Method: "GET",
Project: project,
RawURL: url,
UserAgent: userAgent,
})
if err != nil {
return nil, err
}
ls = append(ls, flattener(res))
pageToken, ok = res["pageToken"]
}
return ls, nil
}
func GetInterconnectAttachmentLink(config *transport_tpg.Config, project, region, ic, userAgent string) (string, error) {
if !strings.Contains(ic, "/") {
icData, err := config.NewComputeClient(userAgent).InterconnectAttachments.Get(
project, region, ic).Do()
if err != nil {
return "", fmt.Errorf("Error reading interconnect attachment: %s", err)
}
ic = icData.SelfLink
}
return ic, nil
}
// Given two sets of references (with "from" values in self link form),
// determine which need to be added or removed // during an update using
// addX/removeX APIs.
func CalcAddRemove(from []string, to []string) (add, remove []string) {
add = make([]string, 0)
remove = make([]string, 0)
for _, u := range to {
found := false
for _, v := range from {
if CompareSelfLinkOrResourceName("", v, u, nil) {
found = true
break
}
}
if !found {
add = append(add, u)
}
}
for _, u := range from {
found := false
for _, v := range to {
if CompareSelfLinkOrResourceName("", u, v, nil) {
found = true
break
}
}
if !found {
remove = append(remove, u)
}
}
return add, remove
}
func StringInSlice(arr []string, str string) bool {
for _, i := range arr {
if i == str {
return true
}
}
return false
}
func MigrateStateNoop(v int, is *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) {
return is, nil
}
func ExpandString(v interface{}, d TerraformResourceData, config *transport_tpg.Config) (string, error) {
return v.(string), nil
}
func ChangeFieldSchemaToForceNew(sch *schema.Schema) {
sch.ForceNew = true
switch sch.Type {
case schema.TypeList:
case schema.TypeSet:
if nestedR, ok := sch.Elem.(*schema.Resource); ok {
for _, nestedSch := range nestedR.Schema {
ChangeFieldSchemaToForceNew(nestedSch)
}
}
}
}
func GenerateUserAgentString(d TerraformResourceData, currentUserAgent string) (string, error) {
var m transport_tpg.ProviderMeta
err := d.GetProviderMeta(&m)
if err != nil {
return currentUserAgent, err
}
if m.ModuleName != "" {
return strings.Join([]string{currentUserAgent, m.ModuleName}, " "), nil
}
return currentUserAgent, nil
}
func SnakeToPascalCase(s string) string {
split := strings.Split(s, "_")
for i := range split {
split[i] = strings.Title(split[i])
}
return strings.Join(split, "")
}
func CheckStringMap(v interface{}) map[string]string {
m, ok := v.(map[string]string)
if ok {
return m
}
return ConvertStringMap(v.(map[string]interface{}))
}
// return a fake 404 so requests get retried or nested objects are considered deleted
func Fake404(reasonResourceType, resourceName string) *googleapi.Error {
return &googleapi.Error{
Code: 404,
Message: fmt.Sprintf("%v object %v not found", reasonResourceType, resourceName),
}
}
// CheckGoogleIamPolicy makes assertions about the contents of a google_iam_policy data source's policy_data attribute
func CheckGoogleIamPolicy(value string) error {
if strings.Contains(value, "\"description\":\"\"") {
return fmt.Errorf("found an empty description field (should be omitted) in google_iam_policy data source: %s", value)
}
return nil
}
func FrameworkDiagsToSdkDiags(fwD fwDiags.Diagnostics) *diag.Diagnostics {
var diags diag.Diagnostics
for _, e := range fwD.Errors() {
diags = append(diags, diag.Diagnostic{
Detail: e.Detail(),
Severity: diag.Error,
Summary: e.Summary(),
})
}
for _, w := range fwD.Warnings() {
diags = append(diags, diag.Diagnostic{
Detail: w.Detail(),
Severity: diag.Warning,
Summary: w.Summary(),
})
}
return &diags
}
func IsEmptyValue(v reflect.Value) bool {
if !v.IsValid() {
return true
}
switch v.Kind() {
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
return v.Len() == 0
case reflect.Bool:
return !v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Interface, reflect.Ptr:
return v.IsNil()
}
return false
}
func ReplaceVars(d TerraformResourceData, config *transport_tpg.Config, linkTmpl string) (string, error) {
return ReplaceVarsRecursive(d, config, linkTmpl, false, 0)
}
// relaceVarsForId shortens variables by running them through GetResourceNameFromSelfLink
// this allows us to use long forms of variables from configs without needing
// custom id formats. For instance:
// accessPolicies/{{access_policy}}/accessLevels/{{access_level}}
// with values:
// access_policy: accessPolicies/foo
// access_level: accessPolicies/foo/accessLevels/bar
// becomes accessPolicies/foo/accessLevels/bar
func ReplaceVarsForId(d TerraformResourceData, config *transport_tpg.Config, linkTmpl string) (string, error) {
return ReplaceVarsRecursive(d, config, linkTmpl, true, 0)
}
// ReplaceVars must be done recursively because there are baseUrls that can contain references to regions
// (eg cloudrun service) there aren't any cases known for 2+ recursion but we will track a run away
// substitution as 10+ calls to allow for future use cases.
func ReplaceVarsRecursive(d TerraformResourceData, config *transport_tpg.Config, linkTmpl string, shorten bool, depth int) (string, error) {
if depth > 10 {
return "", errors.New("Recursive substitution detected")
}
// https://github.com/google/re2/wiki/Syntax
re := regexp.MustCompile("{{([%[:word:]]+)}}")
f, err := BuildReplacementFunc(re, d, config, linkTmpl, shorten)
if err != nil {
return "", err
}
final := re.ReplaceAllStringFunc(linkTmpl, f)
if re.Match([]byte(final)) {
return ReplaceVarsRecursive(d, config, final, shorten, depth+1)
}
return final, nil
}
// This function replaces references to Terraform properties (in the form of {{var}}) with their value in Terraform
// It also replaces {{project}}, {{project_id_or_project}}, {{region}}, and {{zone}} with their appropriate values
// This function supports URL-encoding the result by prepending '%' to the field name e.g. {{%var}}
func BuildReplacementFunc(re *regexp.Regexp, d TerraformResourceData, config *transport_tpg.Config, linkTmpl string, shorten bool) (func(string) string, error) {
var project, projectID, region, zone string
var err error
if strings.Contains(linkTmpl, "{{project}}") {
project, err = GetProject(d, config)
if err != nil {
return nil, err
}
if shorten {
project = strings.TrimPrefix(project, "projects/")
}
}
if strings.Contains(linkTmpl, "{{project_id_or_project}}") {
v, ok := d.GetOkExists("project_id")
if ok {
projectID, _ = v.(string)
}
if projectID == "" {
project, err = GetProject(d, config)
}
if err != nil {
return nil, err
}
if shorten {
project = strings.TrimPrefix(project, "projects/")
projectID = strings.TrimPrefix(projectID, "projects/")
}
}
if strings.Contains(linkTmpl, "{{region}}") {
region, err = GetRegion(d, config)
if err != nil {
return nil, err
}
if shorten {
region = strings.TrimPrefix(region, "regions/")
}
}
if strings.Contains(linkTmpl, "{{zone}}") {
zone, err = GetZone(d, config)
if err != nil {
return nil, err
}
if shorten {
zone = strings.TrimPrefix(zone, "zones/")
}
}
f := func(s string) string {
m := re.FindStringSubmatch(s)[1]
if m == "project" {
return project
}
if m == "project_id_or_project" {
if projectID != "" {
return projectID
}
return project
}
if m == "region" {
return region
}
if m == "zone" {
return zone
}
if string(m[0]) == "%" {
v, ok := d.GetOkExists(m[1:])
if ok {
return url.PathEscape(fmt.Sprintf("%v", v))
}
} else {
v, ok := d.GetOkExists(m)
if ok {
if shorten {
return GetResourceNameFromSelfLink(fmt.Sprintf("%v", v))
} else {
return fmt.Sprintf("%v", v)
}
}
}
// terraform-google-conversion doesn't provide a provider config in tests.
if config != nil {
// Attempt to draw values from the provider config if it's present.
if f := reflect.Indirect(reflect.ValueOf(config)).FieldByName(m); f.IsValid() {
return f.String()
}
}
return ""
}
return f, nil
}
func GetFileMd5Hash(filename string) string {
data, err := ioutil.ReadFile(filename)
if err != nil {
log.Printf("[WARN] Failed to read source file %q. Cannot compute md5 hash for it.", filename)
return ""
}
return GetContentMd5Hash(data)
}
func GetContentMd5Hash(content []byte) string {
h := md5.New()
if _, err := h.Write(content); err != nil {
log.Printf("[WARN] Failed to compute md5 hash for content: %v", err)
}
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
func DefaultProviderProject(_ context.Context, diff *schema.ResourceDiff, meta interface{}) error {
config := meta.(*transport_tpg.Config)
//project
if project := diff.Get("project"); project != nil {
project2, err := GetProjectFromDiff(diff, config)
if err != nil {
return fmt.Errorf("Failed to retrieve project, pid: %s, err: %s", project, err)
}
if CompareSelfLinkRelativePaths("", project.(string), project2, nil) {
return nil
}
err = diff.SetNew("project", project2)
if err != nil {
return err
}
}
return nil
}
func DefaultProviderRegion(_ context.Context, diff *schema.ResourceDiff, meta interface{}) error {
config := meta.(*transport_tpg.Config)
//region
if region := diff.Get("region"); region != nil {
region, err := GetRegionFromDiff(diff, config)
if err != nil {
return fmt.Errorf("Failed to retrieve region, pid: %s, err: %s", region, err)
}
err = diff.SetNew("region", region)
if err != nil {
return err
}
}
return nil
}
func DefaultProviderZone(_ context.Context, diff *schema.ResourceDiff, meta interface{}) error {
config := meta.(*transport_tpg.Config)
// zone
if zone := diff.Get("zone"); zone != nil {
zone, err := GetZoneFromDiff(diff, config)
if err != nil {
return fmt.Errorf("Failed to retrieve zone, pid: %s, err: %s", zone, err)
}
err = diff.SetNew("zone", zone)
if err != nil {
return err
}
}
return nil
}
// id.UniqueId() returns a timestamp + incremental hash
// This function truncates the timestamp to provide a prefix + 9 using
// YYmmdd + last 3 digits of the incremental hash
func ReducedPrefixedUniqueId(prefix string) string {
// uniqueID is timestamp + 8 digit counter (YYYYmmddHHMMSSssss + 12345678)
uniqueId := id.PrefixedUniqueId("")
// last three digits of the counter (678)
counter := uniqueId[len(uniqueId)-3:]
// YYmmdd of date
date := uniqueId[2:8]
return prefix + date + counter
}