Skip to content
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
35 changes: 35 additions & 0 deletions cloud/etc/deploy-storage/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# SPDX-FileCopyrightText: 2025 - Canonical Ltd
# SPDX-License-Identifier: Apache-2.0

terraform {
required_providers {
juju = {
source = "juju/juju"
version = "= 0.23.1"
}
}
}

provider "juju" {}

data "juju_model" "model" {
name = var.model
}

module "backends" {
for_each = var.backends

source = "./modules/backend"

model = data.juju_model.model.uuid

name = each.key
principal_application = each.value.principal_application
charm_name = each.value.charm_name
charm_base = each.value.charm_base
charm_channel = each.value.charm_channel
charm_revision = each.value.charm_revision
charm_config = each.value.charm_config
endpoint_bindings = each.value.endpoint_bindings
secrets = each.value.secrets
}
74 changes: 74 additions & 0 deletions cloud/etc/deploy-storage/modules/backend/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# SPDX-FileCopyrightText: 2025 - Canonical Ltd
# SPDX-License-Identifier: Apache-2.0

terraform {
required_providers {
juju = {
source = "juju/juju"
}
}
}

data "juju_model" "model" {
name = var.model
}

data "juju_application" "cinder-volume" {
name = var.principal_application
model = data.juju_model.model.name
}

resource "juju_secret" "secret" {
model = data.juju_model.model.name
name = "${var.name}-config-secret"
value = {
for k, v in var.secrets : v => var.charm_config[k]
}
}

resource "juju_access_secret" "secret-access" {
model = juju_secret.secret.model
secret_id = juju_secret.secret.secret_id
applications = [juju_application.storage-backend.name]
}

locals {
charm_config = merge(
{ volume-backend-name = var.name },
var.charm_config,
{ for k, v in var.secrets : k => juju_secret.secret.secret_uri }
)
}

# Deploy Storage backend charms
resource "juju_application" "storage-backend" {
name = var.name
model = data.juju_model.model.uuid
units = 1

charm {
name = var.charm_name
channel = var.charm_channel
revision = var.charm_revision
base = var.charm_base
}

config = local.charm_config

endpoint_bindings = var.endpoint_bindings
}

# Integrate Storage backends with cinder-volume
resource "juju_integration" "storage-backend-to-cinder-volume" {
model = data.juju_model.model.name

application {
name = juju_application.storage-backend.name
endpoint = "cinder-volume"
}

application {
name = data.juju_application.cinder-volume.name
endpoint = "cinder-volume"
}
}
59 changes: 59 additions & 0 deletions cloud/etc/deploy-storage/modules/backend/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# SPDX-FileCopyrightText: 2025 - Canonical Ltd
# SPDX-License-Identifier: Apache-2.0

variable "model" {
description = "Name of the machine model to deploy to"
type = string
}

variable "principal_application" {
description = "Name of the principal application to integrate with"
type = string
default = "cinder-volume"
}

variable "charm_name" {
description = "Name of the Storage charm"
type = string
}

variable "charm_base" {
description = "Base for the Storage charm"
type = string
default = "ubuntu@24.04"
}

variable "charm_channel" {
description = "Operator channel for Storage backend deployment"
type = string
default = "latest/edge"
}

variable "charm_revision" {
description = "Operator channel revision for Storage backend deployment"
type = number
default = null
}

variable "name" {
description = "Name of the backend"
type = string
}

variable "endpoint_bindings" {
description = "Endpoint bindings for the applications"
type = set(map(string))
default = null
}

variable "charm_config" {
description = "Operator config for the Storage backend deployment"
type = map(string)
default = {}
}

variable "secrets" {
description = "Map of secret names to create. The key is the config option name, the value is key to use in the secret dict for the value."
type = map(string)
default = {}
}
2 changes: 2 additions & 0 deletions cloud/etc/deploy-storage/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# SPDX-FileCopyrightText: 2025 - Canonical Ltd
# SPDX-License-Identifier: Apache-2.0
22 changes: 22 additions & 0 deletions cloud/etc/deploy-storage/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# SPDX-FileCopyrightText: 2025 - Canonical Ltd
# SPDX-License-Identifier: Apache-2.0

variable "model" {
description = "UUID of the machine model to deploy to"
type = string
}

variable "backends" {
description = "Map of storage backend configurations"
type = map(object({
principal_application = string
charm_name = string
charm_base = string
charm_channel = string
charm_revision = number
charm_config = map(string)
endpoint_bindings = set(map(string))
secrets = map(string)
}))
default = {}
}
19 changes: 19 additions & 0 deletions sunbeam-microcluster/api/apitypes/storage_backends.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Package apitypes provides shared types and structs.
package apitypes

// StorageBackends holds list of StorageBackend type
type StorageBackends []StorageBackend

// StorageBackend structure to hold storage backend details like name and type
type StorageBackend struct {
// Name of the storage backend
Name string `json:"name" yaml:"name"`
// Type of the storage backend
Type string `json:"type" yaml:"type"`
// Config holds backend specific configuration as a json blob
Config string `json:"config" yaml:"config"`
// Name of the principal application this storage backend is associated with
Principal string `json:"principal" yaml:"principal"`
// ModelUUID is the juju model UUID where this storage backend is deployed
ModelUUID string `json:"model-uuid" yaml:"model-uuid"`
}
2 changes: 2 additions & 0 deletions sunbeam-microcluster/api/servers.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ var Servers = map[string]rest.Server{
manifestsCmd,
manifestCmd,
statusCmd,
storageBackendsCmd,
storageBackendCmd,
},
},
{
Expand Down
117 changes: 117 additions & 0 deletions sunbeam-microcluster/api/storage_backends.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package api

import (
"encoding/json"
"net/http"
"net/url"

"github.com/canonical/lxd/lxd/response"
"github.com/canonical/lxd/shared/api"
"github.com/canonical/microcluster/v2/rest"
"github.com/canonical/microcluster/v2/state"
"github.com/gorilla/mux"

"github.com/canonical/snap-openstack/sunbeam-microcluster/access"
"github.com/canonical/snap-openstack/sunbeam-microcluster/api/apitypes"
"github.com/canonical/snap-openstack/sunbeam-microcluster/sunbeam"
)

// /1.0/storage-backend endpoint.
var storageBackendsCmd = rest.Endpoint{
Path: "storage-backend",

Get: access.ClusterCATrustedEndpoint(cmdStorageBackendsGetAll, true),
Post: access.ClusterCATrustedEndpoint(cmdStorageBackendsPost, true),
}

// /1.0/storage-backend/<backend-name> endpoint.
var storageBackendCmd = rest.Endpoint{
Path: "storage-backend/{backendname}",

Get: access.ClusterCATrustedEndpoint(cmdStorageBackendGet, true),
Delete: access.ClusterCATrustedEndpoint(cmdStorageBackendDelete, true),
Put: access.ClusterCATrustedEndpoint(cmdStorageBackendPut, true),
}

func cmdStorageBackendsGetAll(s state.State, r *http.Request) response.Response {

storageBackends, err := sunbeam.ListStorageBackends(r.Context(), s)
if err != nil {
return response.InternalError(err)
}

return response.SyncResponse(true, storageBackends)
}

func cmdStorageBackendsPost(s state.State, r *http.Request) response.Response {
var req apitypes.StorageBackend

err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
return response.InternalError(err)
}

err = sunbeam.AddStorageBackend(r.Context(), s, req.Name, req.Type, req.Principal, req.ModelUUID, req.Config)
if err != nil {
return response.InternalError(err)
}

return response.EmptySyncResponse
}

func cmdStorageBackendGet(s state.State, r *http.Request) response.Response {
var backendName string
backendName, err := url.PathUnescape(mux.Vars(r)["backendname"])
if err != nil {
return response.InternalError(err)
}
backend, err := sunbeam.GetStorageBackend(r.Context(), s, backendName)
if err != nil {
if err, ok := err.(api.StatusError); ok {
if err.Status() == http.StatusNotFound {
return response.NotFound(err)
}
}
return response.InternalError(err)
}

return response.SyncResponse(true, backend)
}

func cmdStorageBackendDelete(s state.State, r *http.Request) response.Response {
backendName, err := url.PathUnescape(mux.Vars(r)["backendname"])
if err != nil {
return response.SmartError(err)
}
err = sunbeam.DeleteStorageBackend(r.Context(), s, backendName)
if err != nil {
if err, ok := err.(api.StatusError); ok {
if err.Status() == http.StatusNotFound {
return response.NotFound(err)
}
}
return response.InternalError(err)
}

return response.EmptySyncResponse
}

func cmdStorageBackendPut(s state.State, r *http.Request) response.Response {
backendName, err := url.PathUnescape(mux.Vars(r)["backendname"])
if err != nil {
return response.SmartError(err)
}

var req apitypes.StorageBackend
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
return response.InternalError(err)
}

err = sunbeam.UpdateStorageBackend(r.Context(), s, backendName, req.Type, req.Config, req.Principal, req.ModelUUID)
if err != nil {
return response.InternalError(err)
}

return response.EmptySyncResponse
}
19 changes: 19 additions & 0 deletions sunbeam-microcluster/database/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var SchemaExtensions = []schema.Update{
JujuUserSchemaUpdate,
ManifestsSchemaUpdate,
AddSystemIDToNodes,
StorageBackendSchemaUpdate,
}

// NodesSchemaUpdate is schema for table nodes
Expand Down Expand Up @@ -96,3 +97,21 @@ ALTER TABLE nodes ADD COLUMN system_id TEXT default '';

return err
}

// StorageBackendSchemaUpdate is schema for table storage_backends
func StorageBackendSchemaUpdate(_ context.Context, tx *sql.Tx) error {
stmt := `
CREATE TABLE storage_backends (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
name TEXT NOT NULL,
type TEXT NOT NULL,
principal TEXT,
model_uuid TEXT,
config TEXT,
UNIQUE(name)
);
`

_, err := tx.Exec(stmt)
return err
}
Loading
Loading