pkg/bundle/vault/pull.go (96 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you 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 vault
import (
"context"
"fmt"
"regexp"
"github.com/hashicorp/vault/api"
bundlev1 "github.com/elastic/harp/api/gen/go/harp/bundle/v1"
"github.com/elastic/harp/pkg/bundle/vault/internal/operation"
"github.com/elastic/harp/pkg/vault/kv"
vpath "github.com/elastic/harp/pkg/vault/path"
"golang.org/x/sync/errgroup"
)
// Pull all given path as a bundle.
func Pull(ctx context.Context, client *api.Client, paths []string, opts ...Option) (*bundlev1.Bundle, error) {
// Check parameters
if client == nil {
return nil, fmt.Errorf("unable to process with nil client")
}
if len(paths) == 0 {
return nil, fmt.Errorf("no path given to pull")
}
// Default values
var (
defaultPrefix = ""
defaultPathInclusions = []*regexp.Regexp{}
defaultPathExclusions = []*regexp.Regexp{}
defaultWithSecretMetadata = false
defaultWithVaultMetadata = false
defaultWorkerCount = int64(4)
)
// Create default option instance
defaultOpts := &options{
prefix: defaultPrefix,
exclusions: defaultPathExclusions,
includes: defaultPathInclusions,
withSecretMetadata: defaultWithSecretMetadata,
withVaultMetadata: defaultWithVaultMetadata,
workerCount: defaultWorkerCount,
}
// Apply option functions
for _, o := range opts {
if err := o(defaultOpts); err != nil {
return nil, fmt.Errorf("unable to apply option %T: %w", o, err)
}
}
// Run the pull process
b, err := runPull(ctx, client, paths, defaultOpts)
if err != nil {
return nil, fmt.Errorf("error occurs during pull process: %w", err)
}
// No error
return b, nil
}
// runPull starts a multithreaded Vault secret puller.
func runPull(ctx context.Context, client *api.Client, paths []string, opts *options) (*bundlev1.Bundle, error) {
var res *bundlev1.Bundle
// Initialize operation
packageChan := make(chan *bundlev1.Package)
// Prepare output
g, gctx := errgroup.WithContext(ctx)
// Preprocess paths
if len(opts.exclusions) > 0 {
paths = collect(paths, opts.exclusions, false)
}
if len(opts.includes) > 0 {
paths = collect(paths, opts.includes, true)
}
// Fork consumer
// Secret packages consumer
g.Go(func() error {
b := &bundlev1.Bundle{}
// Wait for all packages
for p := range packageChan {
b.Packages = append(b.Packages, p)
}
// Assign result
res = b
// No error
return nil
})
// Fork reader
g.Go(func() error {
defer close(packageChan)
gReader, gReaderctx := errgroup.WithContext(gctx)
// Wrap process in a builder to be able to pass p parameter
exportBuilder := func(p string) func() error {
return func() error {
// Create dedicated service reader
service, err := kv.New(client, p, kv.WithVaultMetatadata(opts.withVaultMetadata))
if err != nil {
return fmt.Errorf("unable to prepare vault reader for path '%s': %w", p, err)
}
// Create an exporter
op := operation.Exporter(service, vpath.SanitizePath(p), packageChan, opts.withSecretMetadata, opts.workerCount)
// Run the job
if err := op.Run(gReaderctx); err != nil {
return fmt.Errorf("unable to export secret values for path `%s': %w", p, err)
}
// No error
return nil
}
}
// Generate producers
for _, p := range paths {
// For the process
gReader.Go(exportBuilder(p))
}
// Wait for all producers to finish
if err := gReader.Wait(); err != nil {
return fmt.Errorf("unable to read secrets: %w", err)
}
// No error
return nil
})
// Wait for completion
if err := g.Wait(); err != nil {
return nil, fmt.Errorf("unable to pull secrets: %w", err)
}
// Check bundle result
if res == nil {
return nil, fmt.Errorf("result bundle is nil")
}
// No error
return res, nil
}