Skip to content

Commit

Permalink
[bug] Reuse and close connections per resource
Browse files Browse the repository at this point in the history
This commit changes the way we handle connections
and clients:

- It now creates the connection to the controlplane
  at provider level and passed down to the
  resources.
- It does the same with the authentication token.
- Now, after every method is run, we close the
  connection created with the dataplane. Previously
  we were leaving the connection open causing a
  lot of port usage on every terraform command
  that the user ran.
  • Loading branch information
r-vasquez committed Mar 21, 2024
1 parent dff67f2 commit 73fb2ff
Show file tree
Hide file tree
Showing 17 changed files with 215 additions and 417 deletions.
60 changes: 0 additions & 60 deletions redpanda/clients/controlplane.go

This file was deleted.

73 changes: 0 additions & 73 deletions redpanda/clients/dataplane.go

This file was deleted.

40 changes: 18 additions & 22 deletions redpanda/clients/clients.go → redpanda/cloud/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.

// Package clients provides the CloudV2 clients used by the Redpanda terraform
// provider and the generated resources.
package clients
// Package cloud provides the methods to connect and talk to the Redpanda Cloud
// public API.
package cloud

import (
"context"
Expand All @@ -36,15 +36,15 @@ import (
"google.golang.org/grpc/metadata"
)

// cloudEndpoint is a representation of a cloud V2 endpoint, containing the URLs
// for authentication and the API URL.
type cloudEndpoint struct {
apiURL string // CloudV2 public API URL.
// Endpoint is a representation of a cloud endpoint for a single environment. It
// contains the URLs, audience for authentication and the API URL.
type Endpoint struct {
APIURL string // CloudV2 public API URL.
authURL string // CloudV2 URL for authorization token exchange.
audience string // CloudV2 audience used for token exchange.
}

var cloudAuthEnvironments = map[string]cloudEndpoint{
var endpoints = map[string]Endpoint{
"dev": {
"api.dev.cloud.redpanda.com:443",
"https://dev-cloudv2.us.auth0.com/oauth/token",
Expand All @@ -62,33 +62,27 @@ var cloudAuthEnvironments = map[string]cloudEndpoint{
},
}

// ClientRequest are the client request credentials used to create a connection.
type ClientRequest struct {
ClientID string
ClientSecret string
// TODO: we can use this as the only source of truth for Client Credentials and Envs.
}

type tokenResponse struct {
AccessToken string `json:"access_token"`
Scope string `json:"scope"`
ExpiresIn int `json:"expires_in"`
TokenType string `json:"token_type"`
}

// requestTokenAndEnv requests a token.
func requestTokenAndEnv(ctx context.Context, cloudEnv string, cr ClientRequest) (string, *cloudEndpoint, error) {
if cr.ClientID == "" {
// RequestTokenAndEnv requests an authentication token and return the Endpoint
// for a given environment.
func RequestTokenAndEnv(ctx context.Context, cloudEnv, clientID, clientSecret string) (string, *Endpoint, error) {
if clientID == "" {
return "", nil, fmt.Errorf("client_id is not set")
}
if cr.ClientSecret == "" {
if clientSecret == "" {
return "", nil, fmt.Errorf("client_secret is not set")
}
endpoint, found := cloudAuthEnvironments[cloudEnv]
endpoint, found := endpoints[cloudEnv]
if !found {
return "", nil, fmt.Errorf("unable to find requested environment: %q", cloudEnv)
}
payload := fmt.Sprintf("grant_type=client_credentials&client_id=%s&client_secret=%s&audience=%s", cr.ClientID, cr.ClientSecret, endpoint.audience)
payload := fmt.Sprintf("grant_type=client_credentials&client_id=%s&client_secret=%s&audience=%s", clientID, clientSecret, endpoint.audience)
req, err := http.NewRequestWithContext(ctx, "POST", endpoint.authURL, strings.NewReader(payload))
if err != nil {
return "", nil, fmt.Errorf("unable to issue request to %v: %v", endpoint.authURL, err)
Expand Down Expand Up @@ -118,7 +112,9 @@ func requestTokenAndEnv(ctx context.Context, cloudEnv string, cr ClientRequest)
return tokenContainer.AccessToken, &endpoint, nil
}

func spawnConn(ctx context.Context, url string, authToken string) (*grpc.ClientConn, error) {
// SpawnConn returns a grpc connection to the given URL, it adds a bearer token
// to each request with the given 'authToken'.
func SpawnConn(ctx context.Context, url string, authToken string) (*grpc.ClientConn, error) {
return grpc.DialContext(
ctx,
url,
Expand Down
42 changes: 42 additions & 0 deletions redpanda/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright 2024 Redpanda Data, Inc.
//
//
// 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 config contains the configuration structs to initialize our clients
// and provider.
package config

import "google.golang.org/grpc"

// Resource is the config used to pass data and dependencies to resource
// implementations.
type Resource struct {
AuthToken string
ClientID string
ClientSecret string
CloudEnv string
ControlPlaneConnection *grpc.ClientConn
}

// Datasource is the config used to pass data and dependencies to data source
// implementations.
type Datasource struct {
AuthToken string
ClientID string
ClientSecret string
CloudEnv string
ControlPlaneConnection *grpc.ClientConn
}

// TODO add cloud provider and region as values to persist
40 changes: 31 additions & 9 deletions redpanda/redpanda.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,16 @@ import (
"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/redpanda-data/terraform-provider-redpanda/redpanda/cloud"
"github.com/redpanda-data/terraform-provider-redpanda/redpanda/config"
"github.com/redpanda-data/terraform-provider-redpanda/redpanda/models"
"github.com/redpanda-data/terraform-provider-redpanda/redpanda/resources/acl"
"github.com/redpanda-data/terraform-provider-redpanda/redpanda/resources/cluster"
"github.com/redpanda-data/terraform-provider-redpanda/redpanda/resources/namespace"
"github.com/redpanda-data/terraform-provider-redpanda/redpanda/resources/network"
"github.com/redpanda-data/terraform-provider-redpanda/redpanda/resources/topic"
"github.com/redpanda-data/terraform-provider-redpanda/redpanda/resources/user"
"github.com/redpanda-data/terraform-provider-redpanda/redpanda/utils"
"google.golang.org/grpc"
)

// Ensure provider defined types fully satisfy framework interfaces.
Expand All @@ -46,6 +48,8 @@ type Redpanda struct {
cloudEnv string
// version is the Redpanda terraform provider.
version string
// conn is the connection to the control plane API.
conn *grpc.ClientConn
}

const (
Expand Down Expand Up @@ -118,15 +122,33 @@ func (r *Redpanda) Configure(ctx context.Context, request provider.ConfigureRequ
}
// Clients are passed through to downstream resources through the response
// struct.
response.ResourceData = utils.ResourceData{
ClientID: id,
ClientSecret: sec,
CloudEnv: r.cloudEnv,
token, endpoint, err := cloud.RequestTokenAndEnv(ctx, r.cloudEnv, id, sec)
if err != nil {
response.Diagnostics.AddError("failed to authenticate with Redpanda API", err.Error())
return
}
if r.conn == nil {
conn, err := cloud.SpawnConn(ctx, endpoint.APIURL, token)
if err != nil {
response.Diagnostics.AddError("failed to open a connection with the Redpanda Cloud API", err.Error())
return
}
r.conn = conn
}

response.ResourceData = config.Resource{
AuthToken: token,
ClientID: id,
ClientSecret: sec,
CloudEnv: r.cloudEnv,
ControlPlaneConnection: r.conn,
}
response.DataSourceData = utils.DatasourceData{
ClientID: id,
ClientSecret: sec,
CloudEnv: r.cloudEnv,
response.DataSourceData = config.Datasource{
AuthToken: token,
ClientID: id,
ClientSecret: sec,
CloudEnv: r.cloudEnv,
ControlPlaneConnection: r.conn,
}
}

Expand Down
Loading

0 comments on commit 73fb2ff

Please sign in to comment.