Skip to content
This repository has been archived by the owner on Dec 15, 2022. It is now read-only.

Respect custom timeouts during refresh #276

Merged
merged 3 commits into from
May 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 5 additions & 15 deletions pkg/terraform/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ func (fp *FileProducer) WriteTFState(ctx context.Context) error {
if pr, ok := fp.Resource.GetAnnotations()[resource.AnnotationKeyPrivateRawAttribute]; ok {
privateRaw = []byte(pr)
}
if privateRaw, err = insertTimeoutsMeta(privateRaw, timeouts(fp.Config.OperationTimeouts)); err != nil {
return errors.Wrap(err, "cannot insert timeouts metadata to private raw")
}
s := json.NewStateV4()
s.TerraformVersion = fp.Setup.Version
s.Lineage = string(fp.Resource.GetUID())
Expand Down Expand Up @@ -152,21 +155,8 @@ func (fp *FileProducer) WriteMainTF() error {
}

// Add operation timeouts if any timeout configured for the resource
timeouts := map[string]string{}
if t := fp.Config.OperationTimeouts.Read.String(); t != "0s" {
timeouts["read"] = t
}
if t := fp.Config.OperationTimeouts.Create.String(); t != "0s" {
timeouts["create"] = t
}
if t := fp.Config.OperationTimeouts.Update.String(); t != "0s" {
timeouts["update"] = t
}
if t := fp.Config.OperationTimeouts.Delete.String(); t != "0s" {
timeouts["delete"] = t
}
if len(timeouts) != 0 {
fp.parameters["timeouts"] = timeouts
if tp := timeouts(fp.Config.OperationTimeouts).asParameter(); len(tp) != 0 {
fp.parameters["timeouts"] = tp
}

// Note(turkenh): To use third party providers, we need to configure
Expand Down
65 changes: 48 additions & 17 deletions pkg/terraform/files_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ const (

func TestWriteTFState(t *testing.T) {
type args struct {
tr resource.Terraformed
s Setup
tr resource.Terraformed
cfg *config.Resource
s Setup
}
type want struct {
tfstate string
Expand All @@ -53,33 +54,63 @@ func TestWriteTFState(t *testing.T) {
want
}{
"Success": {
reason: "Standard resources should be able to write everything it has into maintf file",
args: args{tr: &fake.Terraformed{
Managed: xpfake.Managed{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
resource.AnnotationKeyPrivateRawAttribute: "privateraw",
meta.AnnotationKeyExternalName: "some-id",
reason: "Standard resources should be able to write everything it has into tfstate file",
args: args{
tr: &fake.Terraformed{
Managed: xpfake.Managed{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
resource.AnnotationKeyPrivateRawAttribute: "privateraw",
meta.AnnotationKeyExternalName: "some-id",
},
},
},
Parameterizable: fake.Parameterizable{Parameters: map[string]interface{}{
"param": "paramval",
}},
Observable: fake.Observable{Observation: map[string]interface{}{
"obs": "obsval",
}},
},
Parameterizable: fake.Parameterizable{Parameters: map[string]interface{}{
"param": "paramval",
}},
Observable: fake.Observable{Observation: map[string]interface{}{
"obs": "obsval",
}},
}},
cfg: config.DefaultResource("terrajet_resource", nil),
},
want: want{
tfstate: `{"version":4,"terraform_version":"","serial":1,"lineage":"","outputs":null,"resources":[{"mode":"managed","type":"","name":"","provider":"provider[\"registry.terraform.io/\"]","instances":[{"schema_version":0,"attributes":{"id":"some-id","name":"some-id","obs":"obsval","param":"paramval"},"private":"cHJpdmF0ZXJhdw=="}]}]}`,
},
},
"SuccessWithTimeout": {
reason: "Configured timeouts should be reflected tfstate as private meta",
args: args{
tr: &fake.Terraformed{
Managed: xpfake.Managed{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
resource.AnnotationKeyPrivateRawAttribute: "{}",
meta.AnnotationKeyExternalName: "some-id",
},
},
},
Parameterizable: fake.Parameterizable{Parameters: map[string]interface{}{
"param": "paramval",
}},
Observable: fake.Observable{Observation: map[string]interface{}{
"obs": "obsval",
}},
},
cfg: config.DefaultResource("terrajet_resource", nil, func(r *config.Resource) {
r.OperationTimeouts.Read = 2 * time.Minute
}),
},
want: want{
tfstate: `{"version":4,"terraform_version":"","serial":1,"lineage":"","outputs":null,"resources":[{"mode":"managed","type":"","name":"","provider":"provider[\"registry.terraform.io/\"]","instances":[{"schema_version":0,"attributes":{"id":"some-id","name":"some-id","obs":"obsval","param":"paramval"},"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsicmVhZCI6MTIwMDAwMDAwMDAwfX0="}]}]}`,
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
fs := afero.NewMemMapFs()
ctx := context.TODO()
fp, err := NewFileProducer(ctx, nil, dir, tc.args.tr, tc.args.s, config.DefaultResource("terrajet_resource", nil), WithFileSystem(fs))
fp, err := NewFileProducer(ctx, nil, dir, tc.args.tr, tc.args.s, tc.args.cfg, WithFileSystem(fs))
if err != nil {
t.Errorf("cannot initialize a file producer: %s", err.Error())
}
Expand Down
97 changes: 97 additions & 0 deletions pkg/terraform/timeouts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
Copyright 2022 The Crossplane Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package terraform

import (
"github.com/crossplane/crossplane-runtime/pkg/errors"

"github.com/crossplane/terrajet/pkg/config"
"github.com/crossplane/terrajet/pkg/resource/json"
)

// "e2bfb730-ecaa-11e6-8f88-34363bc7c4c0" is a hardcoded string for Terraform
// timeout key in private raw, i.e. provider specific metadata:
// https://github.com/hashicorp/terraform-plugin-sdk/blob/112e2164c381d80e8ada3170dac9a8a5db01079a/helper/schema/resource_timeout.go#L14
const tfMetaTimeoutKey = "e2bfb730-ecaa-11e6-8f88-34363bc7c4c0"

type timeouts config.OperationTimeouts

func (ts timeouts) asParameter() map[string]string {
param := make(map[string]string)
if t := ts.Read.String(); t != "0s" {
param["read"] = t
}
if t := ts.Create.String(); t != "0s" {
param["create"] = t
}
if t := ts.Update.String(); t != "0s" {
param["update"] = t
}
if t := ts.Delete.String(); t != "0s" {
param["delete"] = t
}
return param
}

func (ts timeouts) asMetadata() map[string]interface{} {
// See how timeouts encoded as metadata on Terraform side:
// https://github.com/hashicorp/terraform-plugin-sdk/blob/112e2164c381d80e8ada3170dac9a8a5db01079a/helper/schema/resource_timeout.go#L170
meta := make(map[string]interface{})
if t := ts.Read.String(); t != "0s" {
meta["read"] = ts.Read.Nanoseconds()
}
if t := ts.Create.String(); t != "0s" {
meta["create"] = ts.Create.Nanoseconds()
}
if t := ts.Update.String(); t != "0s" {
meta["update"] = ts.Update.Nanoseconds()
}
if t := ts.Delete.String(); t != "0s" {
meta["delete"] = ts.Delete.Nanoseconds()
}
return meta
}

func insertTimeoutsMeta(existingMeta []byte, to timeouts) ([]byte, error) {
customTimeouts := to.asMetadata()
if len(customTimeouts) == 0 {
// No custom timeout configured, nothing to do.
return existingMeta, nil
}
meta := make(map[string]interface{})
if len(existingMeta) == 0 {
// No existing data, just initialize a new meta with custom timeouts.
meta[tfMetaTimeoutKey] = customTimeouts
return json.JSParser.Marshal(meta)
}
// There are some existing metadata, let's parse it to insert custom
// timeouts properly.
if err := json.JSParser.Unmarshal(existingMeta, &meta); err != nil {
return nil, errors.Wrap(err, "cannot parse existing metadata")
}
if existingTimeouts, ok := meta[tfMetaTimeoutKey].(map[string]interface{}); ok {
// There are some timeout configuration exists in existing metadata.
// Only override custom timeouts.
for k, v := range customTimeouts {
existingTimeouts[k] = v
}
return json.JSParser.Marshal(meta)
}
// No existing timeout configuration, initialize it with custom timeouts.
meta[tfMetaTimeoutKey] = customTimeouts
return json.JSParser.Marshal(meta)
}
Loading