I'm building a Terraform plugin/provider (link) which will help users manage their cloud resources e.g. cloud instances, Kubernetes clusters & etc on a cloud platform.
The cloud platform at this moment does not support Kubernetes nodes size change after it gets created. If user wants to change the nodes size, they need to create a new node pool with the new nodes size.
So I'm adding this block in my plugin code, specifically in the Kubernetes cluster update method (link):
if d.HasChange("target_nodes_size") {
errMsg := []string{
"[ERR] Unable to update 'target_nodes_size' after creation.",
"Please create a new node pool with the new node size.",
}
return fmt.Errorf(strings.Join(errMsg, " "))
}
The problem is, the error only appears when I run terraform apply command. What I want is, I want it to show when user runs terraform plan command so they know it early that it's not possible to change the nodes size without creating a new node pool.
How do I make that target_nodes_size field immutable and show the error early in terraform plan output?
The correct thing to do here is to tell Terraform that changes to the resource cannot be done in place and instead requires the recreation of the resource (normally destroy followed by creation but you can reverse that with lifecycle.create_before_destroy).
When creating a provider you can do this with the ForceNew parameter on a schema's attribute.
As an example, the aws_launch_configuration resource is considered immutable from AWS' API side so every non computed attribute in the schema is marked with ForceNew: true.:
func resourceAwsLaunchConfiguration() *schema.Resource {
return &schema.Resource{
Create: resourceAwsLaunchConfigurationCreate,
Read: resourceAwsLaunchConfigurationRead,
Delete: resourceAwsLaunchConfigurationDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"arn": {
Type: schema.TypeString,
Computed: true,
},
"name": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
ConflictsWith: []string{"name_prefix"},
ValidateFunc: validation.StringLenBetween(1, 255),
},
// ...
If you then attempt to modify any of the ForceNew: true fields then Terraform's plan will show that it needs to replace the resource and at apply time it will automatically do that as long as the user accepts the plan.
For a more complicated example, the aws_elasticsearch_domain resource allows in place version changes but only for specific version upgrade paths (so you can't eg go from 5.4 to 7.8 directly and instead have to go to 5.4 -> 5.6 -> 6.8 -> 7.8. This is done by using the CustomizeDiff attribute on the schema which allows you to use logic at plan time to give a different result than would normally be found from static configuration.
The CustomizeDiff for the aws_elasticsearch_domain elasticsearch_version attribute looks like this:
func resourceAwsElasticSearchDomain() *schema.Resource {
return &schema.Resource{
Create: resourceAwsElasticSearchDomainCreate,
Read: resourceAwsElasticSearchDomainRead,
Update: resourceAwsElasticSearchDomainUpdate,
Delete: resourceAwsElasticSearchDomainDelete,
Importer: &schema.ResourceImporter{
State: resourceAwsElasticSearchDomainImport,
},
Timeouts: &schema.ResourceTimeout{
Update: schema.DefaultTimeout(60 * time.Minute),
},
CustomizeDiff: customdiff.Sequence(
customdiff.ForceNewIf("elasticsearch_version", func(_ context.Context, d *schema.ResourceDiff, meta interface{}) bool {
newVersion := d.Get("elasticsearch_version").(string)
domainName := d.Get("domain_name").(string)
conn := meta.(*AWSClient).esconn
resp, err := conn.GetCompatibleElasticsearchVersions(&elasticsearch.GetCompatibleElasticsearchVersionsInput{
DomainName: aws.String(domainName),
})
if err != nil {
log.Printf("[ERROR] Failed to get compatible ElasticSearch versions %s", domainName)
return false
}
if len(resp.CompatibleElasticsearchVersions) != 1 {
return true
}
for _, targetVersion := range resp.CompatibleElasticsearchVersions[0].TargetVersions {
if aws.StringValue(targetVersion) == newVersion {
return false
}
}
return true
}),
SetTagsDiff,
),
Attempting to upgrade an aws_elasticsearch_domain's elasticsearch_version on an accepted upgrade path (eg 7.4 -> 7.8) will show that it's an in place upgrade in the plan and apply that at apply time. On the other hand if you tried to upgrade via a path that isn't allowed directly (eg 5.4 -> 7.8 directly) then Terraform's plan will show that it needs to destroy the existing Elasticsearch domain and create a new one.
Related
Suppose I have bellow code snippet which setups a reconciler that watches external resource "External":
// SetupWithManager sets up the controller with the Manager.
func (r *SomethingReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&api.Something{}).
WithOptions(controller.Options{
MaxConcurrentReconciles: stdruntime.NumCPU(),
RecoverPanic: true,
}).
Watches(
&source.Kind{Type: &somev1.External{}},
handler.EnqueueRequestsFromMapFunc(r.findInternalObjectsForExternal),
builder.WithPredicates(predicate.Funcs{
UpdateFunc: func(ue event.UpdateEvent) bool { return true },
DeleteFunc: func(de event.DeleteEvent) bool { return true },
}),
).
Complete(r)
}
My problem is that I can not import somev1.External type into my project because importing the go module containing this type would break my current project's dependencies.
Is there a way in kubebuilder to watch for external resources without having to explicitly importing their types? like GVK or something?
I'm trying to set up a simple Batch Compute Environment using a LaunchTemplate, so that I can specify a larger-than-default volume size:
const templateName = 'my-template'
const jobLaunchTemplate = new ec2.LaunchTemplate(stack, 'Template', {
launchTemplateName: templateName,
blockDevices: [ ..vol config .. ]
})
const computeEnv = new batch.CfnComputeEnvironment(stack, 'CompEnvironment', {
type: 'managed',
computeResources: {
instanceRole: jobRole.roleName,
instanceTypes: [
InstanceType.of(InstanceClass.C4, InstanceSize.LARGE).toString()
],
maxvCpus: 64,
minvCpus: 0,
desiredvCpus: 0,
subnets: vpc.publicSubnets.map(sn => sn.subnetId),
securityGroupIds: [vpc.vpcDefaultSecurityGroup],
type: 'EC2',
launchTemplate: {
launchTemplateName: templateName,
}
},
})
They both initialize fine when not linked, however as soon as the launchTemplate block is added to the compute environment, I get the following error:
Error: Resource handler returned message: "Resource of type 'AWS::Batch::ComputeEnvironment' with identifier 'compute-env-arn' did not stabilize." (RequestToken: token, HandlerErrorCode: NotStabilized)
Any suggestions are greatly appreciated, thanks in advance!
For anyone running into this - check the resource that is being created in the AWS Console - i.e go to aws.amazon.com and refresh the page over and over until you see it created by CF. This gave me a different error message regarding the instance profile not existing (A bit more helpful than the terminal error...)
A simple CfnInstanceProfile did the trick:
new iam.CfnInstanceProfile(stack, "batchInstanceProfile", {
instanceProfileName: jobRole.roleName,
roles: [jobRole.roleName],
});
I faced similar error.
But in my case cdk had created subnetGroups list in cdk.context.json and was trying to use the same in the CfnComputeEnvironment definition.
The problem was; I was using the default vpc and had manually modified few subnets. and cdk.context.json was not updated.
Solved by deleting the cdk.context.json
This file was recreated with correct values in next synth.
Tip for others facing similar problem:
Don't just rely on the error message; watch closely the Cloud-formation Script that's generated from CDK for the resource.
I have the following code:
const func = new NodejsFunction(this, <function name>, {
memorySize: 2048,
timeout: Duration.seconds(60),
runtime: Runtime.NODEJS_14_X,
handler: 'handler',
role: <role>,
entry: path.join(__dirname, <filePath>),
currentVersionOptions: {
description: `Version created on ${new Date(Date.now())}`,
},
});
const version = func.currentVersion;
const alias = new Alias(this, 'VersionAlias', {
aliasName: 'current',
version,
});
I do this with a handful of Lambda functions all in the same stack. The first deployment works, however the lambda functions are created with random version numbers (some have v4, some with v5, some with v7).
Subsequent deployments then fail with a vague Internal Failure error message. So I check the CloudTrail logs and find a series of ResourceNotFoundException errors. The "Version" resources are unable to be updated because they have the incorrect version number stemming from the first deploy. How can I force CloudFormation to start at #1 for versioning my lambda functions?
For anyone visiting this later, the problem was with the following code:
currentVersionOptions: {
description: `Version created on ${new Date(Date.now())}`,
},
Apparently you can't have a dynamic description as it is an immutable field
This question already has an answer here:
Create/Get a custom kubernetes resource
(1 answer)
Closed 1 year ago.
I am using Velero to create and backup and restore, Velero has controllers which get triggered when I can create the custom objects.
import veleroApi "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
restoreObj := veleroApi.Restore{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{
DeletionGracePeriodSeconds: &gracePeriodSeconds,
},
Spec: veleroApi.RestoreSpec{
BackupName: "backup-name-20211101",
RestorePVs: &restorePV,
},
Status: veleroApi.RestoreStatus{},
}
But how can I submit this custom object to the Kube API server?
I used API client to apply the changes:
apiClient.CoreV1().RESTClient().Patch(types.ApplyPatchType).Body(restoreObj).Do(context)
But I am getting:
unknown type used for body: {TypeMeta:{Kind:Restore APIVersion:velero.io/v1} ObjectMeta:{Name: GenerateName: Namespace:velero SelfLink: UID: ResourceVersion: Generation:0 CreationTimestamp:0001-01-01 00:00:00 +0000 UTC DeletionTimestamp:<nil> DeletionGracePeriodSeconds:0xc000256018 Labels:map[] Annotations:map[] OwnerReferences:[] Finalizers:[] ClusterName: ManagedFields:[]} Spec:{BackupName:backup-name-20211101 ScheduleName: IncludedNamespaces:[] ExcludedNamespaces:[] IncludedResources:[] ExcludedResources:[] NamespaceMapping:map[] LabelSelector:nil RestorePVs:0xc0007a9088 PreserveNodePorts:<nil> IncludeClusterResources:<nil> Hooks:{Resources:[]}} Status:{Phase: ValidationErrors:[] Warnings:0 Errors:0 FailureReason: StartTimestamp:<nil> CompletionTimestamp:<nil> Progress:<nil>}}
If you would like to create a client for custom object follow the following steps:
Describe the custom resource for which you would like to create a rest client:
kubectl describe CustomResourceDefinition <custom resource definition name>
Note down the API and version and the Kind, as an example it would look like:
API Version: apiextensions.k8s.io/v1
Kind: CustomResourceDefinition
Here, apiextensions.k8s.io is API and v1 is the version.
Check if the API version that you got from step 1 is in the list of APIs:
kubectl get --raw "/"
Create the client:
func getClusterConfig() *rest.Config {
config, err := rest.InClusterConfig()
if err != nil {
glog.Fatal(err.Error())
}
return config
}
func getRestClient() *rest.RESTClient {
cfg := getClusterConfig()
gv := schema.GroupVersion{Group: "<API>", Version: "<version>"}
cfg.GroupVersion = &gv
cfg.APIPath = "/apis" // you can verify the path from step 2
var Scheme = runtime.NewScheme()
var Codecs = serializer.NewCodecFactory(Scheme)
cfg.NegotiatedSerializer = Codecs.WithoutConversion()
restClient, err := rest.RESTClientFor(cfg)
if err != nil {
panic(err.Error())
}
return restClient
}
Alternatively, check the answer from kozmo here
For Velero you can reuse the client they have.
As an example take a look at this code:
restore, err := o.client.VeleroV1().Restores(restore.Namespace).Create(context.TODO(), restore, metav1.CreateOptions{})
I'm trying to work with Istio from Go, and are using Kubernetes and Istio go-client code.
The problem I'm having is that I can't specify ObjectMeta or TypeMeta in my Istio-ServiceRole object. I can only specify rules, which are inside the spec.
Below you can see what I got working:
import (
v1alpha1 "istio.io/api/rbac/v1alpha1"
)
func getDefaultServiceRole(app nais.Application) *v1alpha1.ServiceRole {
return &v1alpha1.ServiceRole{
Rules: []*v1alpha1.AccessRule{
{
Ports: []int32{2},
},
},
}
}
What I would like to do is have this code work:
func getDefaultServiceRole(app *nais.Application) *v1alpha1.ServiceRole {
return &v1alpha1.ServiceRole{
TypeMeta: metav1.TypeMeta{
Kind: "ServiceRole",
APIVersion: "v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: app.Name,
Namespace: app.Namespace,
},
Spec: v1alpha1.ServiceRole{
Rules: []*v1alpha1.AccessRule{
{
Ports: []int32{2},
},
},
},
},
}
Can anyone point me in the right direction?
Ah - this is a pretty painful point: Istio requires Kubernetes CRD wrapper metadata (primarily the name and namespace fields), but those fields are not part of the API objects themselves nor are they represented in the protos. (This is changing with the new MCP API for configuring components - which Galley uses - does encode these fields as protobufs but that doesn't help for your use case.) Instead, you should use the types in istio.io/istio/pilot/pkg/config/kube/crd, which implement the K8s CRD interface.
The easiest way to work with the Istio objects in golang is to use Pilot's libraries, particularly the istio.io/istio/pilot/pkg/model and istio.io/istio/pilot/pkg/config/kube/crd packages, as well as the model.Config struct. You can either pass around the full model.Config (not great because spec has type proto.Message so you need type assertions to extract the data you care about), or pass around the inner object wrap it in a model.Config before you push it. You can use the model.ProtoSchema type to help with conversion to and from YAML and JSON. Pilot only defines ProtoSchema objects for the networking API, the type is public and you can create them for arbitrary types.
So, using your example code I might try something like:
import (
v1alpha1 "istio.io/api/rbac/v1alpha1"
"istio.io/istio/pilot/pkg/model"
)
func getDefaultServiceRole() *v1alpha1.ServiceRole {
return &v1alpha1.ServiceRole{
Rules: []*v1alpha1.AccessRule{
{
Ports: []int32{2},
},
},
}
}
func toConfig(app *nais.Application, role *v1alpha1.ServiceRole) model.Config {
return &model.Config{
ConfigMeta: model.ConfigMeta{
Name: app.Name,
Namespace: app.Namespace,
},
Spec: app,
}
}
type Client model.ConfigStore
func (c Client) CreateRoleFor(app nais.Application, role *v1alpha1.ServiceRole) error {
cfg := toConfig(app, role)
_, err := c.Create(cfg)
return err
}
As a more complete example, we built the Istio CloudMap operator in this style. Here's the core of it that pushes config to K8s with Pilot libraries. Here's the incantation to create an instance of model.ConfigStore to use to create objects. Finally, I want to call out explicitly as it's only implicit in the example: when you call Create on the model.ConfigStore, the ConfigStore relies on the metadata in the ProtoSchema objects used to create it. So be sure to initialize the store with ProtoSchema objects for all of the types you'll be working with.
You can achieve the same using just the K8s client libraries and the istio.io/istio/pilot/pkg/config/kube/crd package, but I have not done it firsthand and don't have examples handy.
Istio now supports:
import (
istiov1alpha3 "istio.io/api/networking/v1alpha3"
istiogov1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3"
)
VirtualService := istiogov1alpha3.VirtualService{
TypeMeta: metav1.TypeMeta{
Kind: "VirtualService",
APIVersion: "networking.istio.io/v1alpha3",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-name",
},
Spec: istiov1alpha3.VirtualService{},
}
Where istiov1alpha3.VirtualService{} is an istio object.