Skip to content

Commit

Permalink
add new remote-builder flag to build with the Function Builder API th…
Browse files Browse the repository at this point in the history
…rough faas-cli

Signed-off-by: Nikhil Sharma <nikhilsharma230303@gmail.com>
  • Loading branch information
NikhilSharmaWe committed Apr 29, 2023
1 parent 0b1e92b commit 7ad71bc
Show file tree
Hide file tree
Showing 8 changed files with 394 additions and 648 deletions.
302 changes: 245 additions & 57 deletions builder/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,95 +4,168 @@
package builder

import (
"archive/tar"
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"path"
"path/filepath"
"strings"

v1execute "github.com/alexellis/go-execute/pkg/v1"
"github.com/openfaas/faas-cli/schema"
"github.com/openfaas/faas-cli/stack"

hmac "github.com/alexellis/hmac/v2"
)

type buildConfig struct {
Image string `json:"image"`
BuildArgs map[string]string `json:"buildArgs,omitempty"`
}

type buildResult struct {
Log []string `json:"log"`
Image string `json:"image"`
Status string `json:"status"`
}

const ConfigFileName = "com.openfaas.docker.config"

// PublishImage will publish images as multi-arch
// TODO: refactor signature to a struct to simplify the length of the method header
func PublishImage(image string, handler string, functionName string, language string, nocache bool, squash bool, shrinkwrap bool, buildArgMap map[string]string,
buildOptions []string, tagMode schema.BuildFormat, buildLabelMap map[string]string, quietBuild bool, copyExtraPaths []string, platforms string, extraTags []string) error {
buildOptions []string, tagMode schema.BuildFormat, buildLabelMap map[string]string, quietBuild bool, copyExtraPaths []string, platforms string, extraTags []string, remoteBuilder string) error {

if stack.IsValidTemplate(language) {
pathToTemplateYAML := fmt.Sprintf("./template/%s/template.yml", language)
if _, err := os.Stat(pathToTemplateYAML); os.IsNotExist(err) {
return err
}

langTemplate, err := stack.ParseYAMLForLanguageTemplate(pathToTemplateYAML)
if err != nil {
return fmt.Errorf("error reading language template: %s", err.Error())
}

branch, version, err := GetImageTagValues(tagMode)
if err != nil {
return err
}

imageName := schema.BuildImageName(tagMode, image, version, branch)

if err := ensureHandlerPath(handler); err != nil {
return fmt.Errorf("building %s, %s is an invalid path", imageName, handler)
}
if remoteBuilder != "" {
tempDir, err := os.MkdirTemp(os.TempDir(), "builder-*")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(tempDir)

tempPath, buildErr := createBuildContext(functionName, handler, language, isLanguageTemplate(language), langTemplate.HandlerFolder, copyExtraPaths)
fmt.Printf("Building: %s with %s template. Please wait..\n", imageName, language)
if buildErr != nil {
return buildErr
}
tarPath := path.Join(tempDir, "req.tar")
fmt.Println(tarPath)

if shrinkwrap {
fmt.Printf("%s shrink-wrapped to %s\n", functionName, tempPath)
return nil
}
if err := shrinkWrap(imageName, handler, language); err != nil {
log.Fatal(err)
}

buildOptPackages, buildPackageErr := getBuildOptionPackages(buildOptions, language, langTemplate.BuildOptions)
if err := makeTar(buildConfig{Image: imageName}, path.Join("build", "context"), tarPath); err != nil {
log.Fatalf("Failed to create tar file: %s", err)
}

if buildPackageErr != nil {
return buildPackageErr
res, err := callBuilder(tarPath, remoteBuilder)
if err != nil {
log.Fatalf("Failed to call builder API: %s", err)
}
defer res.Body.Close()

}
data, _ := io.ReadAll(res.Body)

dockerBuildVal := dockerBuild{
Image: imageName,
NoCache: nocache,
Squash: squash,
HTTPProxy: os.Getenv("http_proxy"),
HTTPSProxy: os.Getenv("https_proxy"),
BuildArgMap: buildArgMap,
BuildOptPackages: buildOptPackages,
BuildLabelMap: buildLabelMap,
Platforms: platforms,
ExtraTags: extraTags,
}
result := buildResult{}
if err := json.Unmarshal(data, &result); err != nil {
log.Fatalf("Failed to unmarshal build result: %s", err)
}

command, args := getDockerBuildxCommand(dockerBuildVal)
fmt.Printf("Publishing with command: %v %v\n", command, args)
if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusAccepted {
log.Fatalf("Unable to build image %s: %s", imageName, result.Status)
}

task := v1execute.ExecTask{
Cwd: tempPath,
Command: command,
Args: args,
StreamStdio: !quietBuild,
}
log.Printf("Success building image: %s", result.Image)

res, err := task.Execute()

if err != nil {
return err
}

if res.ExitCode != 0 {
return fmt.Errorf("[%s] received non-zero exit code from build, error: %s", functionName, res.Stderr)
} else {
pathToTemplateYAML := fmt.Sprintf("./template/%s/template.yml", language)
if _, err := os.Stat(pathToTemplateYAML); os.IsNotExist(err) {
return err
}

langTemplate, err := stack.ParseYAMLForLanguageTemplate(pathToTemplateYAML)
if err != nil {
return fmt.Errorf("error reading language template: %s", err.Error())
}

branch, version, err := GetImageTagValues(tagMode)
if err != nil {
return err
}

imageName := schema.BuildImageName(tagMode, image, version, branch)

if err := ensureHandlerPath(handler); err != nil {
return fmt.Errorf("building %s, %s is an invalid path", imageName, handler)
}

tempPath, buildErr := createBuildContext(functionName, handler, language, isLanguageTemplate(language), langTemplate.HandlerFolder, copyExtraPaths)
fmt.Printf("Building: %s with %s template. Please wait..\n", imageName, language)
if buildErr != nil {
return buildErr
}

if shrinkwrap {
fmt.Printf("%s shrink-wrapped to %s\n", functionName, tempPath)
return nil
}

buildOptPackages, buildPackageErr := getBuildOptionPackages(buildOptions, language, langTemplate.BuildOptions)

if buildPackageErr != nil {
return buildPackageErr

}

dockerBuildVal := dockerBuild{
Image: imageName,
NoCache: nocache,
Squash: squash,
HTTPProxy: os.Getenv("http_proxy"),
HTTPSProxy: os.Getenv("https_proxy"),
BuildArgMap: buildArgMap,
BuildOptPackages: buildOptPackages,
BuildLabelMap: buildLabelMap,
Platforms: platforms,
ExtraTags: extraTags,
}

command, args := getDockerBuildxCommand(dockerBuildVal)
fmt.Printf("Publishing with command: %v %v\n", command, args)

task := v1execute.ExecTask{
Cwd: tempPath,
Command: command,
Args: args,
StreamStdio: !quietBuild,
}

res, err := task.Execute()

if err != nil {
return err
}

if res.ExitCode != 0 {
return fmt.Errorf("[%s] received non-zero exit code from build, error: %s", functionName, res.Stderr)
}

fmt.Printf("Image: %s built.\n", imageName)
}

fmt.Printf("Image: %s built.\n", imageName)

} else {
return fmt.Errorf("language template: %s not supported, build a custom Dockerfile", language)
}
Expand Down Expand Up @@ -132,3 +205,118 @@ func getDockerBuildxCommand(build dockerBuild) (string, []string) {
func applyTag(index int, baseImage, tag string) string {
return fmt.Sprintf("%s:%s", baseImage[:index], tag)
}

func shrinkWrap(image, handler, lang string) error {
buildCmd := exec.Command(
"faas-cli",
"build",
"--lang",
lang,
"--handler",
handler,
"--name",
"context",
"--image",
image,
"--shrinkwrap",
)

err := buildCmd.Start()
if err != nil {
return fmt.Errorf("cannot start faas-cli build: %t", err)
}

err = buildCmd.Wait()
if err != nil {
return fmt.Errorf("failed to shrinkwrap handler")
}

return nil
}

func makeTar(buildConfig buildConfig, base, tarPath string) error {
configBytes, _ := json.Marshal(buildConfig)
if err := ioutil.WriteFile(path.Join(base, ConfigFileName), configBytes, 0664); err != nil {
return err
}

tarFile, err := os.Create(tarPath)
if err != nil {
return err
}

tarWriter := tar.NewWriter(tarFile)
defer tarWriter.Close()

err = filepath.Walk(base, func(path string, f os.FileInfo, pathErr error) error {
if pathErr != nil {
return pathErr
}

targetFile, err := os.Open(path)
if err != nil {
return err
}

header, err := tar.FileInfoHeader(f, f.Name())
if err != nil {
return err
}

header.Name = strings.TrimPrefix(path, base)
if header.Name != fmt.Sprintf("/%s", ConfigFileName) {
header.Name = filepath.Join("context", header.Name)
}

header.Name = strings.TrimPrefix(header.Name, "/")

if err := tarWriter.WriteHeader(header); err != nil {
return err
}

if f.Mode().IsDir() {
return nil
}

_, err = io.Copy(tarWriter, targetFile)
return err
})

return err
}

func callBuilder(tarPath, builderAddress string) (*http.Response, error) {
payloadSecret, err := os.ReadFile("payload.txt")
if err != nil {
return nil, err
}

tarFile, err := os.Open(tarPath)
if err != nil {
return nil, err
}
defer tarFile.Close()

tarFileBytes, err := ioutil.ReadAll(tarFile)
if err != nil {
return nil, err
}

digest := hmac.Sign(tarFileBytes, bytes.TrimSpace(payloadSecret), sha256.New)
fmt.Println(hex.EncodeToString(digest))

r, err := http.NewRequest(http.MethodPost, builderAddress, bytes.NewReader(tarFileBytes))
if err != nil {
return nil, err
}

r.Header.Set("X-Build-Signature", "sha256="+hex.EncodeToString(digest))
r.Header.Set("Content-Type", "application/octet-stream")

res, err := http.DefaultClient.Do(r)
if err != nil {
return nil, err
}

return res, nil
}
Loading

0 comments on commit 7ad71bc

Please sign in to comment.