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

Commit

Permalink
Merge pull request #276 from turkenh/timeouts-to-state
Browse files Browse the repository at this point in the history
Respect custom timeouts during refresh
  • Loading branch information
turkenh authored May 6, 2022
2 parents 4f9db89 + 4cddcfe commit 3b673dc
Show file tree
Hide file tree
Showing 4 changed files with 379 additions and 32 deletions.
20 changes: 5 additions & 15 deletions pkg/terraform/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,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 @@ -151,21 +154,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

0 comments on commit 3b673dc

Please sign in to comment.