Skip to content

Commit

Permalink
Merge pull request #630 from shipperizer/ds/application
Browse files Browse the repository at this point in the history
#630

## Description

Creation of a `juju_application` data source

The data source can be used in cases where deployment topology is split between different teams but there is still a lot of inter-operation between services 

As any data source this prevents hardcoding of values, in this case application names, without validation 


## Type of change

- Add new resource

## Environment

- Juju controller version: `3.5+`

- Terraform version: `1.8.2+`

## QA steps

Manual QA steps should be done to test this PR.

```tf
terraform {
 required_providers {
 juju = {
 source = "juju/juju"
 version = "> 0.14.0"
 }
 }

 required_version = ">= 1.5.0"
}

variable "revision" {
 type = string
}
########################

resource "juju_application" "hydra" {
 model = "test"
 trust = true

 charm {
 name = "hydra"
 channel = "latest/edge"
 base = "ubuntu@22.04"
 revision = var.revision
 }
}


### HYDRA dependent ###
data "juju_application" "hydra" {
 model = juju_application.hydra.model
 name = juju_application.hydra.name
}


```
  • Loading branch information
jujubot authored Jan 13, 2025
2 parents fbd1ccc + 614549b commit 2f955fe
Show file tree
Hide file tree
Showing 7 changed files with 275 additions and 4 deletions.
21 changes: 21 additions & 0 deletions docs/data-sources/application.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "juju_application Data Source - terraform-provider-juju"
subcategory: ""
description: |-
A data source that represents a single Juju application deployment from a charm.
---

# juju_application (Data Source)

A data source that represents a single Juju application deployment from a charm.



<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `model` (String) The name of the model where the application is deployed.
- `name` (String) Name of the application.
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Today this provider allows you to manage the following via resources:

and refer to the following via data sources:

* Applications
* Machines
* Models
* Offers
Expand Down
142 changes: 142 additions & 0 deletions internal/provider/data_source_application.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Copyright 2024 Canonical Ltd.
// Licensed under the Apache License, Version 2.0, see LICENCE file for details.

package provider

import (
"context"
"fmt"

"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"

"github.com/juju/terraform-provider-juju/internal/juju"
)

// Ensure provider defined types fully satisfy framework interfaces.
var _ datasource.DataSourceWithConfigure = &applicationDataSource{}

// NewApplicationDataSource returns a new data source for a Juju application.
func NewApplicationDataSource() datasource.DataSourceWithConfigure {
return &applicationDataSource{}
}

type applicationDataSource struct {
client *juju.Client

// context for the logging subsystem.
subCtx context.Context
}

type applicationDataSourceModel struct {
ApplicationName types.String `tfsdk:"name"`
ModelName types.String `tfsdk:"model"`
}

// Metadata returns the full data source name as used in terraform plans.
func (d *applicationDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_application"
}

// Schema returns the schema for the application data source.
func (d *applicationDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "A data source that represents a single Juju application deployment from a charm.",
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Description: "Name of the application.",
Required: true,
},
"model": schema.StringAttribute{
Description: "The name of the model where the application is deployed.",
Required: true,
},
},
}
}

// Configure enables provider-level data or clients to be set in the
// provider-defined DataSource type. It is separately executed for each
// ReadDataSource RPC.
func (d *applicationDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
// Prevent panic if the provider has not been configured.
if req.ProviderData == nil {
return
}

client, ok := req.ProviderData.(*juju.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *juju.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}

d.client = client
d.subCtx = tflog.NewSubsystem(ctx, LogDataSourceApplication)
}

// Read is called when the provider must read resource values in order
// to update state. Planned state values should be read from the
// ReadRequest and new state values set on the ReadResponse.
// Take the juju api input from the ID, it may not exist in the plan.
// Only set optional values if they exist.
func (d *applicationDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
// Prevent panic if the provider has not been configured.
if d.client == nil {
addDSClientNotConfiguredError(&resp.Diagnostics, "application")
return
}
var data applicationDataSourceModel

// Read Terraform prior data into the application
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}

appName := data.ApplicationName.ValueString()
modelName := data.ModelName.ValueString()
d.trace("Read", map[string]interface{}{
"Model": modelName,
"Name": appName,
})

response, err := d.client.Applications.ReadApplication(&juju.ReadApplicationInput{
ModelName: modelName,
AppName: appName,
})
if err != nil {
resp.Diagnostics.Append(handleApplicationNotFoundError(ctx, err, &resp.State)...)
return
}
if response == nil {
return
}
d.trace("read application", map[string]interface{}{"resource": appName, "response": response})

data.ApplicationName = types.StringValue(appName)
data.ModelName = types.StringValue(modelName)

d.trace("Found", applicationDataSourceModelForLogging(ctx, &data))
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (d *applicationDataSource) trace(msg string, additionalFields ...map[string]interface{}) {
if d.subCtx == nil {
return
}

tflog.SubsystemTrace(d.subCtx, LogDataSourceApplication, msg, additionalFields...)
}

func applicationDataSourceModelForLogging(_ context.Context, app *applicationDataSourceModel) map[string]interface{} {
value := map[string]interface{}{
"application-name": app.ApplicationName.ValueString(),
"model": app.ModelName.ValueString(),
}
return value
}
104 changes: 104 additions & 0 deletions internal/provider/data_source_application_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright 2024 Canonical Ltd.
// Licensed under the Apache License, Version 2.0, see LICENCE file for details.

package provider

import (
"fmt"
"testing"

"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
)

func TestAcc_DataSourceApplicationLXD_Edge(t *testing.T) {
if testingCloud != LXDCloudTesting {
t.Skip(t.Name() + " only runs with LXD")
}
modelName := acctest.RandomWithPrefix("tf-datasource-application-test-model")
applicationName := acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: frameworkProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccDataSourceApplicationLXD(modelName, applicationName),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("data.juju_application.this", "model", modelName),
resource.TestCheckResourceAttr("data.juju_application.this", "name", applicationName),
),
},
},
})
}

func TestAcc_DataSourceApplicationK8s_Edge(t *testing.T) {
if testingCloud != MicroK8sTesting {
t.Skip(t.Name() + " only runs with MicroK8s")
}
modelName := acctest.RandomWithPrefix("tf-datasource-application-test-model")
applicationName := acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: frameworkProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccDataSourceApplicationK8s(modelName, applicationName),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("data.juju_application.this", "model", modelName),
resource.TestCheckResourceAttr("data.juju_application.this", "name", applicationName),
),
},
},
})
}

func testAccDataSourceApplicationLXD(modelName, applicationName string) string {
return fmt.Sprintf(`
resource "juju_model" "model" {
name = %q
}
resource "juju_application" "this" {
name = %q
model = juju_model.model.name
trust = true
charm {
name = "ubuntu"
channel = "latest/stable"
}
}
data "juju_application" "this" {
model = juju_model.model.name
name = juju_application.this.name
}`, modelName, applicationName)
}

func testAccDataSourceApplicationK8s(modelName, applicationName string) string {
return fmt.Sprintf(`
resource "juju_model" "model" {
name = %q
}
resource "juju_application" "this" {
name = %q
model = juju_model.model.name
trust = true
charm {
name = "zinc-k8s"
channel = "latest/stable"
}
}
data "juju_application" "this" {
model = juju_model.model.name
name = juju_application.this.name
}`, modelName, applicationName)
}
9 changes: 5 additions & 4 deletions internal/provider/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ import (
//
// @module=juju.resource-application
const (
LogDataSourceMachine = "datasource-machine"
LogDataSourceModel = "datasource-model"
LogDataSourceOffer = "datasource-offer"
LogDataSourceSecret = "datasource-secret"
LogDataSourceApplication = "datasource-application"
LogDataSourceMachine = "datasource-machine"
LogDataSourceModel = "datasource-model"
LogDataSourceOffer = "datasource-offer"
LogDataSourceSecret = "datasource-secret"

LogResourceApplication = "resource-application"
LogResourceAccessModel = "resource-access-model"
Expand Down
1 change: 1 addition & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,7 @@ func (p *jujuProvider) Resources(_ context.Context) []func() resource.Resource {
// the Metadata method. All data sources must have unique names.
func (p *jujuProvider) DataSources(_ context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{
func() datasource.DataSource { return NewApplicationDataSource() },
func() datasource.DataSource { return NewMachineDataSource() },
func() datasource.DataSource { return NewModelDataSource() },
func() datasource.DataSource { return NewOfferDataSource() },
Expand Down
1 change: 1 addition & 0 deletions templates/index.md.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Today this provider allows you to manage the following via resources:

and refer to the following via data sources:

* Applications
* Machines
* Models
* Offers
Expand Down

0 comments on commit 2f955fe

Please sign in to comment.