blob: 59ebf4ea820642e7466a48f4ab5f89bee7062614 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF 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 dubbo
import (
"bufio"
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
"github.com/apache/dubbo-kubernetes/app/dubboctl/internal/kube"
"github.com/spf13/cobra"
"github.com/apache/dubbo-kubernetes/app/dubboctl/internal/util"
"gopkg.in/yaml.v2"
)
const (
// DefaultTemplate is the default function signature / environmental context
// of the resultant function. All runtimes are expected to have at least
// one implementation of each supported function signature. Currently that
// includes common
DefaultTemplate = "common"
)
type Client struct {
repositoriesPath string // path to repositories
repositoriesURI string // repo URI (overrides repositories path)
templates *Templates // Templates management
repositories *Repositories // Repositories management
builder Builder // Builds a runnable image source
pusher Pusher // Pushes function image to a remote
deployer Deployer // Deploys or Updates a function}
KubeCtl *kube.CtlClient // Kube Client
}
// Builder of function source to runnable image.
type Builder interface {
// Build a function project with source located at path.
Build(context.Context, *Dubbo) error
}
// Pusher of function image to a registry.
type Pusher interface {
// Push the image of the function.
// Returns Image Digest - SHA256 hash of the produced image
Push(ctx context.Context, f *Dubbo) (string, error)
}
// Deployer of function source to running status.
type Deployer interface {
// Deploy a function of given name, using given backing image.
Deploy(context.Context, *Dubbo, ...DeployOption) (DeploymentResult, error)
}
type DeploymentResult struct {
Status Status
Namespace string
}
type Status int
const (
Failed Status = iota
Deployed
)
// Repositories accessor
func (c *Client) Repositories() *Repositories {
return c.repositories
}
// Templates accessor
func (c *Client) Templates() *Templates {
return c.templates
}
// Runtimes available in totality.
// Not all repository/template combinations necessarily exist,
// and further validation is performed when a template+runtime is chosen.
// from a given repository. This is the global list of all available.
// Returned list is unique and sorted.
func (c *Client) Runtimes() ([]string, error) {
runtimes := util.NewSortedSet()
// Gather all runtimes from all repositories
// into a uniqueness map
repositories, err := c.Repositories().All()
if err != nil {
return []string{}, err
}
for _, repo := range repositories {
for _, runtime := range repo.Runtimes {
runtimes.Add(runtime.Name)
}
}
// Return a unique, sorted list of runtimes
return runtimes.Items(), nil
}
// Option defines a function which when passed to the Client constructor
// optionally mutates private members at time of instantiation.
type Option func(*Client)
func WithKubeClient(client *kube.CtlClient) Option {
return func(c *Client) {
c.KubeCtl = client
}
}
func WithPusher(pusher Pusher) Option {
return func(c *Client) {
c.pusher = pusher
}
}
// WithRepository sets a specific URL to a Git repository from which to pull
// templates. This setting's existence precldes the use of either the inbuilt
// templates or any repositories from the extensible repositories path.
func WithRepository(uri string) Option {
return func(c *Client) {
c.repositoriesURI = uri
}
}
// WithBuilder provides the concrete implementation of a builder.
func WithBuilder(d Builder) Option {
return func(c *Client) {
c.builder = d
}
}
func WithDeployer(d Deployer) Option {
return func(c *Client) {
c.deployer = d
}
}
// WithRepositoriesPath sets the location on disk to use for extensible template
// repositories. Extensible template repositories are additional templates
// that exist on disk and are not built into the binary.
func WithRepositoriesPath(path string) Option {
return func(c *Client) {
c.repositoriesPath = path
}
}
// Init Initialize a new function with the given function struct defaults.
// Does not build/push/deploy. Local FS changes only. For higher-level
// control see New or Apply.
//
// <path> will default to the absolute path of the current working directory.
// <name> will default to the current working directory.
// When <name> is provided but <path> is not, a directory <name> is created
// in the current working directory and used for <path>.
func (c *Client) Init(cfg *Dubbo, init bool, cmd *cobra.Command) (*Dubbo, error) {
// convert Root path to absolute
var err error
oldRoot := cfg.Root
cfg.Root, err = filepath.Abs(cfg.Root)
if err != nil {
return cfg, err
}
// Create project root directory, if it doesn't already exist
if err = os.MkdirAll(cfg.Root, 0o755); err != nil {
return cfg, err
}
// Create should never clobber a pre-existing function
hasApp, err := hasInitializedApplication(cfg.Root)
if err != nil {
return cfg, err
}
if hasApp {
return cfg, fmt.Errorf("application at '%v' already initialized", cfg.Root)
}
// Path is defaulted to the current working directory
if cfg.Root == "" {
if cfg.Root, err = os.Getwd(); err != nil {
return cfg, err
}
}
// Name is defaulted to the directory of the given path.
if cfg.Name == "" {
cfg.Name = nameFromPath(cfg.Root)
}
if !init {
// The path for the new function should not have any contentious files
// (hidden files OK, unless it's one used by dubbo)
if err := assertEmptyRoot(cfg.Root); err != nil {
return cfg, err
}
}
// Create a new application (in memory)
f := NewDubboWith(cfg, init)
// Create a .dubbo directory which is also added to a .gitignore
if err = EnsureRunDataDir(f.Root); err != nil {
return f, err
}
if !init {
// Write out the new function's Template files.
// Templates contain values which may result in the function being mutated
// (default builders, etc), so a new (potentially mutated) function is
// returned from Templates.Write
err = c.Templates().Write(f)
if err != nil {
return f, err
}
}
f.Created = time.Now()
err = f.Write()
if err != nil {
return f, err
}
err = f.EnsureDockerfile(cmd)
if err != nil {
return f, err
}
// Load the now-initialized application.
return NewDubbo(oldRoot)
}
// EnsureRunDataDir creates a .dubbo directory at the given path, and
// registers it as ignored in a .gitignore file.
func EnsureRunDataDir(root string) error {
// Ensure the runtime directory exists
if err := os.MkdirAll(filepath.Join(root, RunDataDir), os.ModePerm); err != nil {
return err
}
// Update .gitignore
//
// Ensure .dubbo is added to .gitignore unless the user explicitly
// commented out the ignore line for some awful reason.
// Also creates the .gitignore in the application's root directory if it does
// not already exist (note that this may not be in the root of the repo
// if the function is at a subpart of a monorepo)
filePath := filepath.Join(root, ".gitignore")
roFile, err := os.Open(filePath)
if err != nil && !os.IsNotExist(err) {
return err
}
defer roFile.Close()
if !os.IsNotExist(err) { // if no error openeing it
s := bufio.NewScanner(roFile) // create a scanner
for s.Scan() { // scan each line
if strings.HasPrefix(s.Text(), "# /"+RunDataDir) { // if it was commented
return nil // user wants it
}
if strings.HasPrefix(s.Text(), "#/"+RunDataDir) {
return nil // user wants it
}
if strings.HasPrefix(s.Text(), "/"+RunDataDir) { // if it is there
return nil // we're done
}
}
}
// Either .gitignore does not exist or it does not have the ignore
// directive for .func yet.
roFile.Close()
rwFile, err := os.OpenFile(filePath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o644)
if err != nil {
return err
}
defer rwFile.Close()
if _, err = rwFile.WriteString(`
# Applications use the .dubbo directory for local runtime data which should
# generally not be tracked in source control. To instruct the system to track
# .dubbo in source control, comment the following line (prefix it with '# ').
/.dubbo
`); err != nil {
return err
}
// Flush to disk immediately since this may affect subsequent calculations
// of the build stamp
if err = rwFile.Sync(); err != nil {
fmt.Fprintf(os.Stderr, "warning: error when syncing .gitignore. %s\n", err)
}
return nil
}
// returns true if the given path contains an initialized function.
func hasInitializedApplication(path string) (bool, error) {
var err error
filename := filepath.Join(path, DubboFile)
if _, err = os.Stat(filename); err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err // invalid path or access error
}
bb, err := os.ReadFile(filename)
if err != nil {
return false, err
}
f := Dubbo{}
if err = yaml.Unmarshal(bb, &f); err != nil {
return false, err
}
return f.Initialized(), nil
}
// New client for function management.
func New(options ...Option) *Client {
// Instantiate client with static defaults.
c := &Client{}
for _, o := range options {
o(c)
}
// Initialize sub-managers using now-fully-initialized client.
c.repositories = newRepositories(c)
c.templates = newTemplates(c)
return c
}
type BuildOptions struct{}
type BuildOption func(c *BuildOptions)
// Build the function at path. Errors if the function is either unloadable or does
// not contain a populated Image.
func (c *Client) Build(ctx context.Context, f *Dubbo, options ...BuildOption) (*Dubbo, error) {
fmt.Fprintln(os.Stderr, "Building application image")
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// Options for the build task
oo := BuildOptions{}
for _, o := range options {
o(&oo)
}
var err error
if err = c.builder.Build(ctx, f); err != nil {
return f, err
}
if err = f.Stamp(); err != nil {
return f, err
}
// use by the cli for user echo
fmt.Fprintf(os.Stderr, "🙌 Application built: %v\n", f.Image)
return f, err
}
type DeployParams struct {
skipBuiltCheck bool
}
type DeployOption func(f *DeployParams)
func (c *Client) Deploy(ctx context.Context, d *Dubbo, opts ...DeployOption) (*Dubbo, error) {
deployParams := &DeployParams{skipBuiltCheck: false}
for _, opt := range opts {
opt(deployParams)
}
go func() {
<-ctx.Done()
}()
// Application must be built (have an associated image) before being deployed.
// Note that externally built images may be specified in the dubbo.yaml
// if !deployParams.skipBuiltCheck && !d.Built() {
// return d, ErrNotBuilt
//}
// Application must have a name to be deployed (a path on the network at which
// it should take up residence.
if d.Name == "" {
return d, ErrNameRequired
}
fmt.Fprintln(os.Stderr, "⬆️ Deploying application to the cluster or generate manifest")
result, err := c.deployer.Deploy(ctx, d)
if err != nil {
fmt.Printf("deploy error: %v\n", err)
return d, err
}
// Update the function with the namespace into which the function was
// deployed
d.Deploy.Namespace = result.Namespace
if result.Status == Deployed {
fmt.Fprintf(os.Stderr, "✅ Application deployed in namespace %q or manifest had been generated\n", result.Namespace)
}
return d, nil
}
// BuildStamp accesses the current (last) build stamp for the function.
// Inbuilt functions return empty string.
func (f Dubbo) BuildStamp() string {
path := filepath.Join(f.Root, RunDataDir, "built")
if _, err := os.Stat(path); err != nil {
return ""
}
b, err := os.ReadFile(path)
if err != nil {
return ""
}
return string(b)
}
// Built returns true if the application is considered built.
// Note that this only considers the application as it exists on-disk at
// f.Root.
func (f *Dubbo) Built() bool {
// If there is no build stamp, it is not built.
stamp := f.BuildStamp()
if stamp == "" {
return false
}
// Missing an image name always means !Built (but does not satisfy staleness
// checks).
// NOTE: This will be updated in the future such that a build does not
// automatically update the function's serialized, source-controlled state,
// because merely building does not indicate the function has changed, but
// rather that field should be populated on deploy. I.e. the Image name
// and image stamp should reside as transient data in .func until such time
// as the given image has been deployed.
// An example of how this bug manifests is that every rebuild of a function
// registers the func.yaml as being dirty for source-control purposes, when
// this should only happen on deploy.
if f.Image == "" {
return false
}
// Calculate the current filesystem hash and see if it has changed.
//
// If this comparison returns true, the Function has a populated image,
// existing builds-tamp, and the calculated fingerprint has not changed.
//
// It's a pretty good chance the thing doesn't need to be rebuilt, though
// of course filesystem racing conditions do exist, including both direct
// source code modifications or changes to the image cache.
hash, _, err := Fingerprint(f)
if err != nil {
fmt.Fprintf(os.Stderr, "error calculating function's fingerprint: %v\n", err)
return false
}
return stamp == hash
}
// Push the image for the named service to the configured registry
func (c *Client) Push(ctx context.Context, f *Dubbo) (*Dubbo, error) {
if !f.Built() {
return f, ErrNotBuilt
}
var err error
if f.ImageDigest, err = c.pusher.Push(ctx, f); err != nil {
return f, err
}
return f, nil
}
// DEFAULTS
// ---------
// Manual implementations (noobs) of required interfaces.
// In practice, the user of this client package (for example the CLI) will
// provide a concrete implementation for only the interfaces necessary to
// complete the given command. Integrators importing the package would
// provide a concrete implementation for all interfaces to be used. To
// enable partial definition (in particular used for testing) they
// are defaulted to noop implementations such that they can be provided
// only when necessary. Unit tests for the concrete implementations
// serve to keep the core logic here separate from the imperative, and
// with a minimum of external dependencies.
// -----------------------------------------------------
// Builder
type noopBuilder struct{ output io.Writer }
func (n *noopBuilder) Build(ctx context.Context, _ Dubbo) error { return nil }
// Pusher
type noopPusher struct{ output io.Writer }
func (n *noopPusher) Push(ctx context.Context, f *Dubbo) error { return nil }
// Deployer
type noopDeployer struct{ output io.Writer }
func (n *noopDeployer) Deploy(ctx context.Context, _ *Dubbo, option ...DeployOption) (DeploymentResult, error) {
return DeploymentResult{}, nil
}