Skip to content

Commit

Permalink
ovirt,openstack: use env instead of files for secrets
Browse files Browse the repository at this point in the history
Use envFrom to pass secrets to the populator controller. This simplifies
the code, and makes the openstack test introduced in the patch easier to
implement.

Signed-off-by: Benny Zlotnik <[email protected]>
  • Loading branch information
bennyz committed Dec 3, 2023
1 parent 8ebc20d commit 5f2bd7a
Show file tree
Hide file tree
Showing 3 changed files with 213 additions and 97 deletions.
243 changes: 191 additions & 52 deletions cmd/openstack-populator/openstack-populator.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,69 @@
package main

import (
"crypto/tls"
"crypto/x509"
"errors"
"flag"
"io"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"

libclient "github.com/konveyor/forklift-controller/pkg/lib/client/openstack"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack"
"github.com/gophercloud/gophercloud/openstack/imageservice/v2/imagedata"
"github.com/gophercloud/utils/openstack/clientconfig"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"

"k8s.io/klog/v2"
)

const (
regionName = "regionName"
authTypeString = "authType"
username = "username"
userID = "userID"
password = "password"
applicationCredentialID = "applicationCredentialID"
applicationCredentialName = "applicationCredentialName"
applicationCredentialSecret = "applicationCredentialSecret"
token = "token"
systemScope = "systemScope"
projectName = "projectName"
projectID = "projectID"
userDomainName = "userDomainName"
userDomainID = "userDomainID"
projectDomainName = "projectDomainName"
projectDomainID = "projectDomainID"
domainName = "domainName"
domainID = "domainID"
defaultDomain = "defaultDomain"
insecureSkipVerify = "insecureSkipVerify"
caCert = "cacert"
endpointAvailability = "availability"
)

const (
unsupportedAuthTypeErrStr = "unsupported authentication type"
malformedCAErrStr = "CA certificate is malformed, failed to configure the CA cert pool"
)

var supportedAuthTypes = map[string]clientconfig.AuthType{
"password": clientconfig.AuthPassword,
"token": clientconfig.AuthToken,
"applicationcredential": clientconfig.AuthV3ApplicationCredential,
}

func main() {
var (
identityEndpoint string
imageID string
crNamespace string
crName string
secretName string

volumePath string
Expand All @@ -31,11 +74,8 @@ func main() {
// Main arg
flag.StringVar(&identityEndpoint, "endpoint", "", "endpoint URL (https://openstack.example.com:5000/v2.0)")
flag.StringVar(&secretName, "secret-name", "", "secret containing OpenStack credentials")

flag.StringVar(&imageID, "image-id", "", "Openstack image ID")
flag.StringVar(&volumePath, "volume-path", "", "Path to populate")
flag.StringVar(&crName, "cr-name", "", "Custom Resource instance name")
flag.StringVar(&crNamespace, "cr-namespace", "", "Custom Resource instance namespace")

flag.Parse()

Expand All @@ -44,7 +84,11 @@ func main() {

func populate(fileName, identityEndpoint, secretName, imageID string) {
http.Handle("/metrics", promhttp.Handler())
go http.ListenAndServe(":2112", nil)
go func() {
if err := http.ListenAndServe(":2112", nil); err != nil {
klog.Error(err)
}
}()
progressGague := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Subsystem: "volume_populators",
Expand All @@ -60,27 +104,27 @@ func populate(fileName, identityEndpoint, secretName, imageID string) {
klog.Info("Prometheus progress counter registered.")
}

options, err := readOptions()
availability := gophercloud.AvailabilityPublic
if a := getStringFromSecret(endpointAvailability); a != "" {
availability = gophercloud.Availability(a)
}
endpointOpts := gophercloud.EndpointOpts{
Region: getStringFromSecret(regionName),
Availability: availability,
}
provider, err := getProviderClient(identityEndpoint)
if err != nil {
klog.Fatal(err)
}

client := &libclient.Client{
URL: identityEndpoint,
Options: options,
}

err = client.Connect()
imageService, err := openstack.NewImageServiceV2(provider, endpointOpts)
if err != nil {
klog.Fatal(err)
}

klog.Info("Downloading the image: ", imageID)
imageReader, err := client.DownloadImage(imageID)
image, err := imagedata.Download(imageService, imageID).Extract()
if err != nil {
klog.Fatal(err)
}
defer imageReader.Close()
defer image.Close()

if err != nil {
klog.Fatal(err)
Expand All @@ -89,42 +133,38 @@ func populate(fileName, identityEndpoint, secretName, imageID string) {
if strings.HasSuffix(fileName, "disk.img") {
flags |= os.O_CREATE
}

klog.Info("Saving the image to: ", fileName)
file, err := os.OpenFile(fileName, flags, 0650)
f, err := os.OpenFile(fileName, flags, 0650)
if err != nil {
klog.Fatal(err)
}
defer file.Close()
defer f.Close()

err = writeData(imageReader, file, imageID, progressGague)
err = writeData(image, f, imageID, progressGague)
if err != nil {
klog.Fatal(err)
}
}

type CountingReader struct {
type countingReader struct {
reader io.ReadCloser
total *int64
}

func (cr *CountingReader) Read(p []byte) (int, error) {
func (cr *countingReader) Read(p []byte) (int, error) {
n, err := cr.reader.Read(p)
*cr.total += int64(n)
return n, err
}

func writeData(reader io.ReadCloser, file *os.File, imageID string, progress *prometheus.GaugeVec) error {
total := new(int64)
countingReader := CountingReader{reader, total}
countingReader := countingReader{reader, total}

done := make(chan bool)
go func() {
for {
select {
case <-done:
klog.Info("Total: ", *total)
klog.Info("Finished!")
return
default:
progress.WithLabelValues(imageID).Set(float64(*total))
Expand All @@ -143,34 +183,133 @@ func writeData(reader io.ReadCloser, file *os.File, imageID string, progress *pr
return nil
}

func readOptions() (options map[string]string, err error) {
options = map[string]string{}
secretDirPath := "/etc/secret-volume"
dirEntries, err := os.ReadDir(secretDirPath)
func getAuthType() (authType clientconfig.AuthType, err error) {
if configuredAuthType := getStringFromSecret(authTypeString); configuredAuthType == "" {
authType = clientconfig.AuthPassword
} else if supportedAuthType, found := supportedAuthTypes[configuredAuthType]; found {
authType = supportedAuthType
} else {
err = errors.New(unsupportedAuthTypeErrStr)
klog.Fatal(err.Error(), "authType", configuredAuthType)
}
return
}

func getStringFromSecret(key string) string {
value := os.Getenv(key)
return string(value)
}

func getBoolFromSecret(key string) bool {
if keyStr := getStringFromSecret(key); keyStr != "" {
value, err := strconv.ParseBool(keyStr)
if err != nil {
return false
}
return value
}
return false
}

func getProviderClient(identityEndpoint string) (provider *gophercloud.ProviderClient, err error) {

authInfo := &clientconfig.AuthInfo{
AuthURL: identityEndpoint,
ProjectName: getStringFromSecret(projectName),
ProjectID: getStringFromSecret(projectID),
UserDomainName: getStringFromSecret(userDomainName),
UserDomainID: getStringFromSecret(userDomainID),
ProjectDomainName: getStringFromSecret(projectDomainName),
ProjectDomainID: getStringFromSecret(projectDomainID),
DomainName: getStringFromSecret(domainName),
DomainID: getStringFromSecret(domainID),
DefaultDomain: getStringFromSecret(defaultDomain),
AllowReauth: true,
}

var authType clientconfig.AuthType
authType, err = getAuthType()
if err != nil {
klog.Fatal(err.Error())
return
}
klog.Info("Options:")
for _, dirEntry := range dirEntries {
if !dirEntry.Type().IsDir() {
option := dirEntry.Name()
if strings.HasPrefix(option, "..") {
continue
}
filePath := filepath.Join(secretDirPath, option)
var fileContent []byte
fileContent, err = os.ReadFile(filePath)
if err != nil {
return
}
value := string(fileContent)
options[option] = value
if option == "password" || option == "applicationCredentialSecret" || option == "token" {
value = strings.Repeat("*", len(value))

switch authType {
case clientconfig.AuthPassword:
authInfo.Username = getStringFromSecret(username)
authInfo.UserID = getStringFromSecret(userID)
authInfo.Password = getStringFromSecret(password)
case clientconfig.AuthToken:
authInfo.Token = getStringFromSecret(token)
case clientconfig.AuthV3ApplicationCredential:
authInfo.Username = getStringFromSecret(username)
authInfo.ApplicationCredentialID = getStringFromSecret(applicationCredentialID)
authInfo.ApplicationCredentialName = getStringFromSecret(applicationCredentialName)
authInfo.ApplicationCredentialSecret = getStringFromSecret(applicationCredentialSecret)
}

identityURL, err := url.Parse(identityEndpoint)
if err != nil {
klog.Fatal(err.Error())
return
}

var TLSClientConfig *tls.Config
if identityURL.Scheme == "https" {
if getBoolFromSecret(insecureSkipVerify) {
TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
} else {
cacert := []byte(getStringFromSecret(caCert))
if len(cacert) == 0 {
klog.Info("CA certificate was not provided,system CA cert pool is used")
} else {
roots := x509.NewCertPool()
ok := roots.AppendCertsFromPEM(cacert)
if !ok {
err = errors.New(malformedCAErrStr)
klog.Fatal(err.Error())
return
}
TLSClientConfig = &tls.Config{RootCAs: roots}
}
klog.Info(" - ", option, " = ", value)

}
}

provider, err = openstack.NewClient(identityEndpoint)
if err != nil {
klog.Fatal(err.Error())
return
}

provider.HTTPClient.Transport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 10 * time.Second,
}).DialContext,
MaxIdleConns: 10,
IdleConnTimeout: 10 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: TLSClientConfig,
}

clientOpts := &clientconfig.ClientOpts{
AuthType: authType,
AuthInfo: authInfo,
}

opts, err := clientconfig.AuthOptions(clientOpts)
if err != nil {
klog.Fatal(err.Error())
return
}

err = openstack.Authenticate(provider, *opts)
if err != nil {
klog.Fatal(err.Error())
return
}
return
}
Loading

0 comments on commit 5f2bd7a

Please sign in to comment.