Skip to content

Commit fb4a031

Browse files
Merge branch 'master' into chore-fix-typo-entitlement_control
2 parents 11eb1e1 + 562b47f commit fb4a031

File tree

5 files changed

+237
-34
lines changed

5 files changed

+237
-34
lines changed

cloudsmith/resource_repository_upstream.go

Lines changed: 103 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"net/http"
7+
"os"
78
"strings"
89
"time"
910

@@ -49,13 +50,16 @@ const (
4950
UpstreamType = "upstream_type"
5051
UpstreamUrl = "upstream_url"
5152
VerifySsl = "verify_ssl"
53+
AuthCertificateKey = "auth_certificate_key"
54+
AuthCertificate = "auth_certificate"
5255
)
5356

5457
var (
5558
authModes = []string{
5659
"None",
5760
"Username and Password",
5861
"Token",
62+
"Certificate and Key",
5963
}
6064
upstreamModes = []string{
6165
"Proxy Only",
@@ -113,6 +117,43 @@ func importUpstream(_ context.Context, d *schema.ResourceData, _ interface{}) ([
113117
return []*schema.ResourceData{d}, nil
114118
}
115119

120+
// readCertificateFiles reads the certificate and key files for mTLS authentication
121+
// Returns pointers to the certificate and key contents, and any error encountered
122+
func readCertificateFiles(d *schema.ResourceData) (cert, key *string, err error) {
123+
if certPath := optionalString(d, AuthCertificate); certPath != nil {
124+
certContent, err := os.ReadFile(*certPath)
125+
if err != nil {
126+
return nil, nil, fmt.Errorf("error reading auth certificate file: %w", err)
127+
}
128+
// Remove any trailing whitespace and ensure proper PEM format
129+
certStr := strings.TrimSpace(string(certContent))
130+
if !strings.HasPrefix(certStr, "-----BEGIN CERTIFICATE-----") {
131+
return nil, nil, fmt.Errorf("invalid certificate format: must be a PEM encoded certificate")
132+
}
133+
cert = &certStr
134+
}
135+
136+
if certKeyPath := optionalString(d, AuthCertificateKey); certKeyPath != nil {
137+
certKeyContent, err := os.ReadFile(*certKeyPath)
138+
if err != nil {
139+
return nil, nil, fmt.Errorf("error reading auth certificate key file: %w", err)
140+
}
141+
// Remove any trailing whitespace and ensure proper PEM format
142+
certKeyStr := strings.TrimSpace(string(certKeyContent))
143+
if !strings.HasPrefix(certKeyStr, "-----BEGIN") || !strings.Contains(certKeyStr, "PRIVATE KEY-----") {
144+
return nil, nil, fmt.Errorf("invalid private key format: must be a PEM encoded private key")
145+
}
146+
key = &certKeyStr
147+
}
148+
149+
// Both certificate and key must be provided together
150+
if (cert != nil && key == nil) || (cert == nil && key != nil) {
151+
return nil, nil, fmt.Errorf("both auth_certificate and auth_certificate_key must be provided when using Certificate and Key authentication")
152+
}
153+
154+
return cert, key, nil
155+
}
156+
116157
func resourceRepositoryUpstreamCreate(d *schema.ResourceData, m interface{}) error {
117158
pc := m.(*providerConfig)
118159

@@ -217,22 +258,34 @@ func resourceRepositoryUpstreamCreate(d *schema.ResourceData, m interface{}) err
217258
upstream, resp, err = pc.APIClient.ReposApi.ReposUpstreamDebCreateExecute(req)
218259
case Docker:
219260
req := pc.APIClient.ReposApi.ReposUpstreamDockerCreate(pc.Auth, namespace, repository)
261+
262+
// Read certificate files for mTLS authentication (Docker only for now)
263+
authCert, authCertKey, err := readCertificateFiles(d)
264+
if err != nil {
265+
return err
266+
}
267+
220268
req = req.Data(cloudsmith.DockerUpstreamRequest{
221-
AuthMode: authMode,
222-
AuthSecret: authSecret,
223-
AuthUsername: authUsername,
224-
ExtraHeader1: extraHeader1,
225-
ExtraHeader2: extraHeader2,
226-
ExtraValue1: extraValue1,
227-
ExtraValue2: extraValue2,
228-
IsActive: isActive,
229-
Mode: mode,
230-
Name: name,
231-
Priority: priority,
232-
UpstreamUrl: upstreamUrl,
233-
VerifySsl: verifySsl,
269+
AuthMode: authMode,
270+
AuthSecret: authSecret,
271+
AuthUsername: authUsername,
272+
AuthCertificate: authCert,
273+
AuthCertificateKey: authCertKey,
274+
ExtraHeader1: extraHeader1,
275+
ExtraHeader2: extraHeader2,
276+
ExtraValue1: extraValue1,
277+
ExtraValue2: extraValue2,
278+
IsActive: isActive,
279+
Mode: mode,
280+
Name: name,
281+
Priority: priority,
282+
UpstreamUrl: upstreamUrl,
283+
VerifySsl: verifySsl,
234284
})
235285
upstream, resp, err = pc.APIClient.ReposApi.ReposUpstreamDockerCreateExecute(req)
286+
if err != nil {
287+
return err
288+
}
236289
case Helm:
237290
req := pc.APIClient.ReposApi.ReposUpstreamHelmCreate(pc.Auth, namespace, repository)
238291
req = req.Data(cloudsmith.HelmUpstreamRequest{
@@ -622,22 +675,34 @@ func resourceRepositoryUpstreamUpdate(d *schema.ResourceData, m interface{}) err
622675
upstream, _, err = pc.APIClient.ReposApi.ReposUpstreamDebUpdateExecute(req)
623676
case Docker:
624677
req := pc.APIClient.ReposApi.ReposUpstreamDockerUpdate(pc.Auth, namespace, repository, slugPerm)
678+
679+
// Read certificate files for mTLS authentication (Docker only for now)
680+
authCert, authCertKey, err := readCertificateFiles(d)
681+
if err != nil {
682+
return err
683+
}
684+
625685
req = req.Data(cloudsmith.DockerUpstreamRequest{
626-
AuthMode: authMode,
627-
AuthSecret: authSecret,
628-
AuthUsername: authUsername,
629-
ExtraHeader1: extraHeader1,
630-
ExtraHeader2: extraHeader2,
631-
ExtraValue1: extraValue1,
632-
ExtraValue2: extraValue2,
633-
IsActive: isActive,
634-
Mode: mode,
635-
Name: name,
636-
Priority: priority,
637-
UpstreamUrl: upstreamUrl,
638-
VerifySsl: verifySsl,
686+
AuthMode: authMode,
687+
AuthSecret: authSecret,
688+
AuthUsername: authUsername,
689+
AuthCertificate: authCert,
690+
AuthCertificateKey: authCertKey,
691+
ExtraHeader1: extraHeader1,
692+
ExtraHeader2: extraHeader2,
693+
ExtraValue1: extraValue1,
694+
ExtraValue2: extraValue2,
695+
IsActive: isActive,
696+
Mode: mode,
697+
Name: name,
698+
Priority: priority,
699+
UpstreamUrl: upstreamUrl,
700+
VerifySsl: verifySsl,
639701
})
640702
upstream, _, err = pc.APIClient.ReposApi.ReposUpstreamDockerUpdateExecute(req)
703+
if err != nil {
704+
return err
705+
}
641706
case Helm:
642707
req := pc.APIClient.ReposApi.ReposUpstreamHelmUpdate(pc.Auth, namespace, repository, slugPerm)
643708
req = req.Data(cloudsmith.HelmUpstreamRequest{
@@ -922,6 +987,18 @@ func resourceRepositoryUpstream() *schema.Resource {
922987
Optional: true,
923988
ValidateFunc: validation.StringIsNotEmpty,
924989
},
990+
AuthCertificate: {
991+
Type: schema.TypeString,
992+
Description: "Path to the X.509 Certificate file to use for mTLS authentication against the upstream (Docker only)",
993+
Optional: true,
994+
ValidateFunc: validation.StringIsNotEmpty,
995+
},
996+
AuthCertificateKey: {
997+
Type: schema.TypeString,
998+
Description: "Path to the Certificate key file to use for mTLS authentication against the upstream (Docker only)",
999+
Optional: true,
1000+
ValidateFunc: validation.StringIsNotEmpty,
1001+
},
9251002
Component: {
9261003
Type: schema.TypeString,
9271004
Description: "(deb only) The component to fetch from the upstream.",

cloudsmith/resource_repository_upstream_test.go

Lines changed: 128 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,18 @@
22
package cloudsmith
33

44
import (
5+
"crypto/rand"
6+
"crypto/rsa"
7+
"crypto/x509"
8+
"crypto/x509/pkix"
9+
"encoding/pem"
510
"fmt"
611
"io"
12+
"math/big"
713
"net/http"
14+
"os"
815
"testing"
16+
"time"
917

1018
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
1119
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
@@ -222,15 +230,15 @@ resource "cloudsmith_repository_upstream" "ubuntu" {
222230
func TestAccRepositoryUpstreamDocker_basic(t *testing.T) {
223231
t.Parallel()
224232

225-
const dockerUpstreamResourceName = "cloudsmith_repository_upstream.dockerhub"
233+
const dockerUpstreamResourceName = "cloudsmith_repository_upstream.fakedocker"
226234

227235
testAccRepositoryPythonUpstreamConfigBasic := fmt.Sprintf(`
228236
resource "cloudsmith_repository" "test" {
229237
name = "terraform-acc-test-upstream-docker"
230238
namespace = "%s"
231239
}
232240
233-
resource "cloudsmith_repository_upstream" "dockerhub" {
241+
resource "cloudsmith_repository_upstream" "fakedocker" {
234242
namespace = cloudsmith_repository.test.namespace
235243
repository = cloudsmith_repository.test.slug
236244
name = cloudsmith_repository.test.name
@@ -248,7 +256,7 @@ resource "cloudsmith_repository_upstream" "dockerhub" {
248256
namespace = "%s"
249257
}
250258
251-
resource "cloudsmith_repository_upstream" "dockerhub" {
259+
resource "cloudsmith_repository_upstream" "fakedocker" {
252260
auth_mode = "Username and Password"
253261
auth_secret = "SuperSecretPassword123!"
254262
auth_username = "jonny.tables"
@@ -268,6 +276,38 @@ resource "cloudsmith_repository_upstream" "dockerhub" {
268276
}
269277
`, namespace)
270278

279+
// Generate test certificates for mTLS authentication
280+
certPath, keyPath, err := generateTestCertificateAndKey()
281+
if err != nil {
282+
t.Fatalf("Failed to generate test certificates: %v", err)
283+
}
284+
defer func() {
285+
os.Remove(certPath)
286+
os.Remove(keyPath)
287+
}()
288+
289+
testAccRepositoryPythonUpstreamConfigCert := fmt.Sprintf(`
290+
resource "cloudsmith_repository" "test" {
291+
name = "terraform-acc-test-upstream-docker"
292+
namespace = "%s"
293+
}
294+
295+
resource "cloudsmith_repository_upstream" "fakedocker" {
296+
auth_mode = "Certificate and Key"
297+
auth_certificate = "%s"
298+
auth_certificate_key = "%s"
299+
is_active = false
300+
mode = "Cache and Proxy"
301+
name = cloudsmith_repository.test.name
302+
namespace = cloudsmith_repository.test.namespace
303+
priority = 5
304+
repository = cloudsmith_repository.test.slug
305+
upstream_type = "docker"
306+
upstream_url = "https://fake.docker.io"
307+
verify_ssl = true
308+
}
309+
`, namespace, certPath, keyPath)
310+
271311
resource.Test(t, resource.TestCase{
272312
PreCheck: func() { testAccPreCheck(t) },
273313
Providers: testAccProviders,
@@ -309,8 +349,23 @@ resource "cloudsmith_repository_upstream" "dockerhub" {
309349
),
310350
},
311351
{
312-
ResourceName: dockerUpstreamResourceName,
313-
ImportState: true,
352+
Config: testAccRepositoryPythonUpstreamConfigCert,
353+
Check: resource.ComposeTestCheckFunc(
354+
resource.TestCheckResourceAttr(dockerUpstreamResourceName, AuthMode, "Certificate and Key"),
355+
resource.TestCheckResourceAttr(dockerUpstreamResourceName, AuthCertificate, certPath),
356+
resource.TestCheckResourceAttr(dockerUpstreamResourceName, AuthCertificateKey, keyPath),
357+
resource.TestCheckResourceAttr(dockerUpstreamResourceName, IsActive, "false"),
358+
resource.TestCheckResourceAttr(dockerUpstreamResourceName, Priority, "5"),
359+
),
360+
},
361+
{
362+
ResourceName: dockerUpstreamResourceName,
363+
ImportState: true,
364+
ImportStateVerify: true,
365+
ImportStateVerifyIgnore: []string{
366+
"auth_certificate",
367+
"auth_certificate_key",
368+
},
314369
ImportStateIdFunc: func(s *terraform.State) (string, error) {
315370
resourceState := s.RootModule().Resources[dockerUpstreamResourceName]
316371
return fmt.Sprintf(
@@ -321,7 +376,6 @@ resource "cloudsmith_repository_upstream" "dockerhub" {
321376
resourceState.Primary.Attributes[SlugPerm],
322377
), nil
323378
},
324-
ImportStateVerify: true,
325379
},
326380
},
327381
})
@@ -1254,3 +1308,71 @@ func testAccRepositoryUpstreamCheckDestroy(resourceName string) resource.TestChe
12541308
return nil
12551309
}
12561310
}
1311+
func generateTestCertificateAndKey() (certPath string, keyPath string, err error) {
1312+
// Generate a private key
1313+
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
1314+
if err != nil {
1315+
return "", "", fmt.Errorf("failed to generate private key: %w", err)
1316+
}
1317+
1318+
// Create a temporary file for the private key
1319+
keyFile, err := os.CreateTemp("", "test-key-*.pem")
1320+
if err != nil {
1321+
return "", "", fmt.Errorf("failed to create temp key file: %w", err)
1322+
}
1323+
keyPath = keyFile.Name()
1324+
1325+
// Encode and write the private key
1326+
keyPEM := &pem.Block{
1327+
Type: "RSA PRIVATE KEY",
1328+
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
1329+
}
1330+
if err := pem.Encode(keyFile, keyPEM); err != nil {
1331+
os.Remove(keyPath)
1332+
return "", "", fmt.Errorf("failed to write private key: %w", err)
1333+
}
1334+
keyFile.Close()
1335+
1336+
// Create certificate template
1337+
template := x509.Certificate{
1338+
SerialNumber: big.NewInt(1),
1339+
Subject: pkix.Name{
1340+
CommonName: "test.cloudsmith.io",
1341+
Organization: []string{"Cloudsmith Test"},
1342+
},
1343+
NotBefore: time.Now(),
1344+
NotAfter: time.Now().Add(time.Hour * 24), // 24 hour validity
1345+
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
1346+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
1347+
BasicConstraintsValid: true,
1348+
}
1349+
1350+
// Create the certificate
1351+
certBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
1352+
if err != nil {
1353+
os.Remove(keyPath)
1354+
return "", "", fmt.Errorf("failed to create certificate: %w", err)
1355+
}
1356+
1357+
// Create a temporary file for the certificate
1358+
certFile, err := os.CreateTemp("", "test-cert-*.pem")
1359+
if err != nil {
1360+
os.Remove(keyPath)
1361+
return "", "", fmt.Errorf("failed to create temp cert file: %w", err)
1362+
}
1363+
certPath = certFile.Name()
1364+
1365+
// Encode and write the certificate
1366+
certPEM := &pem.Block{
1367+
Type: "CERTIFICATE",
1368+
Bytes: certBytes,
1369+
}
1370+
if err := pem.Encode(certFile, certPEM); err != nil {
1371+
os.Remove(keyPath)
1372+
os.Remove(certPath)
1373+
return "", "", fmt.Errorf("failed to write certificate: %w", err)
1374+
}
1375+
certFile.Close()
1376+
1377+
return certPath, keyPath, nil
1378+
}

docs/resources/repository_upstream.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,9 +195,11 @@ The following arguments are supported:
195195

196196
| Argument | Required | Type | Enumeration | Description |
197197
|:-----------------------:|:--------:|:------------:|:-----------------------------------------------------------------------------------------------------------------------:|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|
198-
| `auth_mode` | N | string | `"None"`<br>`"Username and Password"`<br>`"Token"` | The authentication mode to use when accessing the upstream. |
198+
| `auth_mode` | N | string | `"None"`<br>`"Username and Password"`<br>`"Token"`<br>`"Certificate and Key"` | The authentication mode to use when accessing the upstream. |
199199
| `auth_secret` | N | string | N/A | Used in conjunction with an `auth_mode` of `"Username and Password"` or `"Token"` to hold the password or token used when accessing the upstream. |
200200
| `auth_username` | N | string | N/A | Used only in conjunction with an `auth_mode` of `"Username and Password"` to declare the username used when accessing the upstream. |
201+
| `auth_certificate` | N | string | N/A | Used only in conjunction with an `auth_mode` of `"Certificate and Key"` to specify the path to the certificate file for mTLS authentication. |
202+
| `auth_certificate_key` | N | string | N/A | Used only in conjunction with an `auth_mode` of `"Certificate and Key"` to specify the path to the certificate key file for mTLS authentication. |
201203
| `component` | N | string | N/A | Used only in conjunction with an `upstream_type` of `"deb"` to declare the [component](https://wiki.debian.org/DebianRepository/Format#Components) to fetch from the upstream. |
202204
| `distro_version` | N | string | N/A | Used only in conjunction with an `upstream_type` of `"rpm"` to declare the distribution/version that packages found on this upstream will be associated with. |
203205
| `distro_versions` | N | list<string> | N/A | Used only in conjunction with an `upstream_type` of `"deb"` to declare the array of distributions/versions that packages found on this upstream will be associated with. |

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module github.com/cloudsmith-io/terraform-provider-cloudsmith
33
go 1.19
44

55
require (
6-
github.com/cloudsmith-io/cloudsmith-api-go v0.0.42-0.20241129202450-bd5381591ce5
6+
github.com/cloudsmith-io/cloudsmith-api-go v0.0.42
77
github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320
88
github.com/hashicorp/terraform-plugin-sdk/v2 v2.24.1
99
github.com/samber/lo v1.36.0

0 commit comments

Comments
 (0)