- examples/big-suite: E-Commerce 10 functions, 6 layers, real depends_on - provider: nodejs packages now wrapped in ESM zip (buildJSDeployZip) - provider: loadPackageLiteral reads raw file, nodejs zip via source_dir - all 10 functions verified working: python/node/ruby/go runtimes
590 lines
20 KiB
Go
590 lines
20 KiB
Go
package resources
|
||
|
||
import (
|
||
"archive/zip"
|
||
"bytes"
|
||
"context"
|
||
"crypto/sha256"
|
||
"encoding/base64"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"os"
|
||
"path/filepath"
|
||
|
||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
|
||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||
|
||
"terraform-provider-fission/internal/client"
|
||
)
|
||
|
||
var _ resource.Resource = &PackageResource{}
|
||
var _ resource.ResourceWithImportState = &PackageResource{}
|
||
var _ resource.ResourceWithModifyPlan = &PackageResource{}
|
||
|
||
// Изменено: 2026-04-14 19:45 UTC.
|
||
// Resource для управления Fission Package через Kubernetes CRD API.
|
||
type PackageResource struct {
|
||
client *client.Client
|
||
}
|
||
|
||
// packageResourceModel описывает состояние terraform ресурса fission_package.
|
||
type packageResourceModel struct {
|
||
ID types.String `tfsdk:"id"`
|
||
Name types.String `tfsdk:"name"`
|
||
Environment types.String `tfsdk:"environment"`
|
||
SourceDir types.String `tfsdk:"source_dir"`
|
||
CodePath types.String `tfsdk:"code_path"`
|
||
CodeHash types.String `tfsdk:"code_hash"`
|
||
BuildCmd types.String `tfsdk:"build_command"`
|
||
DeployType types.String `tfsdk:"deploy_type"`
|
||
Namespace types.String `tfsdk:"namespace"`
|
||
UID types.String `tfsdk:"uid"`
|
||
BuildStatus types.String `tfsdk:"build_status"`
|
||
BuildLog types.String `tfsdk:"build_log"`
|
||
}
|
||
|
||
// NewPackageResource создает инстанс ресурса пакета.
|
||
func NewPackageResource() resource.Resource {
|
||
return &PackageResource{}
|
||
}
|
||
|
||
// Metadata задает имя ресурса fission_package.
|
||
func (r *PackageResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||
resp.TypeName = req.ProviderTypeName + "_package"
|
||
}
|
||
|
||
// Schema задает Terraform schema для Package.
|
||
func (r *PackageResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||
resp.Schema = schema.Schema{
|
||
Attributes: map[string]schema.Attribute{
|
||
"id": schema.StringAttribute{
|
||
Computed: true,
|
||
Description: "Идентификатор ресурса (namespace/name).",
|
||
},
|
||
"name": schema.StringAttribute{
|
||
Required: true,
|
||
Description: "Имя Fission Package.",
|
||
},
|
||
"environment": schema.StringAttribute{
|
||
Required: true,
|
||
Description: "Имя Fission Environment, связанного с пакетом.",
|
||
},
|
||
"source_dir": schema.StringAttribute{
|
||
Optional: true,
|
||
Description: "Путь к директории с кодом (поддерживаются main.py, main.js, main.go, main.php, main.rb, main.pl).",
|
||
},
|
||
"code_path": schema.StringAttribute{
|
||
Optional: true,
|
||
Description: "Путь к готовому файлу исходника для literal deployment.",
|
||
},
|
||
"code_hash": schema.StringAttribute{
|
||
Optional: true,
|
||
Computed: true,
|
||
Description: "Произвольный хеш кода для контроля изменений.",
|
||
},
|
||
"build_command": schema.StringAttribute{
|
||
Optional: true,
|
||
Description: "Команда сборки пакета в Fission.",
|
||
},
|
||
"deploy_type": schema.StringAttribute{
|
||
Optional: true,
|
||
Computed: true,
|
||
Default: stringdefault.StaticString("literal"),
|
||
Description: "Тип деплоя: 'literal' (default) — код в deployment.literal, 'source' — код в source.literal (для Go и языков с build step).",
|
||
},
|
||
"namespace": schema.StringAttribute{
|
||
Optional: true,
|
||
Computed: true,
|
||
Description: "Namespace, где создается Package. По умолчанию используется provider namespace.",
|
||
},
|
||
"uid": schema.StringAttribute{
|
||
Computed: true,
|
||
Description: "UID Kubernetes объекта Package.",
|
||
},
|
||
"build_status": schema.StringAttribute{
|
||
Computed: true,
|
||
Description: "Текущий статус сборки пакета из поля status.buildstatus.",
|
||
},
|
||
"build_log": schema.StringAttribute{
|
||
Computed: true,
|
||
Description: "Лог сборки из поля status.buildlog.",
|
||
},
|
||
},
|
||
}
|
||
}
|
||
|
||
// ModifyPlan пересчитывает code_hash по локальному коду, чтобы terraform видел изменения source_dir/code_path.
|
||
func (r *PackageResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
|
||
if req.Plan.Raw.IsNull() {
|
||
return
|
||
}
|
||
|
||
var plan packageResourceModel
|
||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||
if resp.Diagnostics.HasError() {
|
||
return
|
||
}
|
||
|
||
var config packageResourceModel
|
||
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
|
||
if resp.Diagnostics.HasError() {
|
||
return
|
||
}
|
||
|
||
if hasManualCodeHash(config.CodeHash) {
|
||
return
|
||
}
|
||
|
||
if plan.SourceDir.IsUnknown() || plan.CodePath.IsUnknown() {
|
||
return
|
||
}
|
||
|
||
if !validatePackageSource(plan.SourceDir, plan.CodePath, &resp.Diagnostics) {
|
||
return
|
||
}
|
||
|
||
literalBytes, err := loadPackageContent(plan.SourceDir.ValueString(), plan.CodePath.ValueString(), plan.DeployType.ValueString())
|
||
if err != nil {
|
||
resp.Diagnostics.AddError("Ошибка чтения исходного кода пакета", err.Error())
|
||
return
|
||
}
|
||
|
||
plan.CodeHash = types.StringValue(calculateCodeHash(literalBytes))
|
||
resp.Diagnostics.Append(resp.Plan.Set(ctx, &plan)...)
|
||
}
|
||
|
||
// Configure получает клиент из provider.Configure().
|
||
func (r *PackageResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||
if req.ProviderData == nil {
|
||
return
|
||
}
|
||
|
||
fissionClient, ok := req.ProviderData.(*client.Client)
|
||
if !ok {
|
||
resp.Diagnostics.AddError(
|
||
"Некорректный тип provider data",
|
||
fmt.Sprintf("Ожидался *client.Client, получен: %T", req.ProviderData),
|
||
)
|
||
return
|
||
}
|
||
|
||
r.client = fissionClient
|
||
}
|
||
|
||
// Create создает новый Package CRD объект.
|
||
func (r *PackageResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||
var plan packageResourceModel
|
||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||
if resp.Diagnostics.HasError() {
|
||
return
|
||
}
|
||
|
||
namespace := resolveNamespace(plan.Namespace, r.client.Namespace)
|
||
if !validatePackageSource(plan.SourceDir, plan.CodePath, &resp.Diagnostics) {
|
||
return
|
||
}
|
||
|
||
if err := ensureEnvironmentExists(ctx, r.client, namespace, plan.Environment.ValueString()); err != nil {
|
||
resp.Diagnostics.AddError("Ошибка валидации Environment для Package", err.Error())
|
||
return
|
||
}
|
||
|
||
literalBytes, err := loadPackageContent(plan.SourceDir.ValueString(), plan.CodePath.ValueString(), plan.DeployType.ValueString())
|
||
if err != nil {
|
||
resp.Diagnostics.AddError("Ошибка чтения исходного кода пакета", err.Error())
|
||
return
|
||
}
|
||
|
||
if !hasManualCodeHash(plan.CodeHash) {
|
||
plan.CodeHash = types.StringValue(calculateCodeHash(literalBytes))
|
||
}
|
||
|
||
packageObject := packageToUnstructured(plan, namespace, literalBytes)
|
||
createdPackage, err := r.client.CreatePackage(ctx, packageObject)
|
||
if err != nil {
|
||
resp.Diagnostics.AddError("Ошибка создания Fission Package", err.Error())
|
||
return
|
||
}
|
||
|
||
state := unstructuredToPackageModel(createdPackage, plan)
|
||
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||
}
|
||
|
||
// Read синхронизирует Terraform state с текущим состоянием Package CRD.
|
||
func (r *PackageResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||
var state packageResourceModel
|
||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||
if resp.Diagnostics.HasError() {
|
||
return
|
||
}
|
||
|
||
namespace := resolveNamespace(state.Namespace, r.client.Namespace)
|
||
packageObject, err := r.client.GetPackage(ctx, namespace, state.Name.ValueString())
|
||
if err != nil {
|
||
if client.IsNotFound(err) {
|
||
resp.State.RemoveResource(ctx)
|
||
return
|
||
}
|
||
|
||
resp.Diagnostics.AddError("Ошибка чтения Fission Package", err.Error())
|
||
return
|
||
}
|
||
|
||
updatedState := unstructuredToPackageModel(packageObject, state)
|
||
resp.Diagnostics.Append(resp.State.Set(ctx, &updatedState)...)
|
||
}
|
||
|
||
// Update обновляет существующий Package CRD объект.
|
||
func (r *PackageResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||
var plan packageResourceModel
|
||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||
if resp.Diagnostics.HasError() {
|
||
return
|
||
}
|
||
|
||
namespace := resolveNamespace(plan.Namespace, r.client.Namespace)
|
||
if !validatePackageSource(plan.SourceDir, plan.CodePath, &resp.Diagnostics) {
|
||
return
|
||
}
|
||
|
||
if err := ensureEnvironmentExists(ctx, r.client, namespace, plan.Environment.ValueString()); err != nil {
|
||
resp.Diagnostics.AddError("Ошибка валидации Environment для Package", err.Error())
|
||
return
|
||
}
|
||
|
||
literalBytes, err := loadPackageContent(plan.SourceDir.ValueString(), plan.CodePath.ValueString(), plan.DeployType.ValueString())
|
||
if err != nil {
|
||
resp.Diagnostics.AddError("Ошибка чтения исходного кода пакета", err.Error())
|
||
return
|
||
}
|
||
|
||
if !hasManualCodeHash(plan.CodeHash) {
|
||
plan.CodeHash = types.StringValue(calculateCodeHash(literalBytes))
|
||
}
|
||
|
||
existingPackage, err := r.client.GetPackage(ctx, namespace, plan.Name.ValueString())
|
||
if err != nil {
|
||
resp.Diagnostics.AddError("Ошибка получения Package перед обновлением", err.Error())
|
||
return
|
||
}
|
||
|
||
packageObject := packageToUnstructured(plan, namespace, literalBytes)
|
||
packageObject.SetResourceVersion(existingPackage.GetResourceVersion())
|
||
|
||
updatedPackage, err := r.client.UpdatePackage(ctx, packageObject)
|
||
if err != nil {
|
||
resp.Diagnostics.AddError("Ошибка обновления Fission Package", err.Error())
|
||
return
|
||
}
|
||
|
||
state := unstructuredToPackageModel(updatedPackage, plan)
|
||
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||
}
|
||
|
||
// Delete удаляет Package CRD объект.
|
||
func (r *PackageResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||
var state packageResourceModel
|
||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||
if resp.Diagnostics.HasError() {
|
||
return
|
||
}
|
||
|
||
namespace := resolveNamespace(state.Namespace, r.client.Namespace)
|
||
err := r.client.DeletePackage(ctx, namespace, state.Name.ValueString())
|
||
if err != nil && !client.IsNotFound(err) {
|
||
resp.Diagnostics.AddError("Ошибка удаления Fission Package", err.Error())
|
||
}
|
||
}
|
||
|
||
// ImportState поддерживает импорт ресурса в формате namespace/name.
|
||
func (r *PackageResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||
namespace, name, err := parseImportID(req.ID)
|
||
if err != nil {
|
||
resp.Diagnostics.AddError("Некорректный формат import ID", err.Error())
|
||
return
|
||
}
|
||
|
||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), req.ID)...)
|
||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("namespace"), namespace)...)
|
||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), name)...)
|
||
}
|
||
|
||
// validatePackageSource проверяет, что задан ровно один источник кода: source_dir или code_path.
|
||
func validatePackageSource(sourceDir, codePath types.String, diagnostics *diag.Diagnostics) bool {
|
||
hasSourceDir := !sourceDir.IsNull() && !sourceDir.IsUnknown() && sourceDir.ValueString() != ""
|
||
hasCodePath := !codePath.IsNull() && !codePath.IsUnknown() && codePath.ValueString() != ""
|
||
|
||
if hasSourceDir == hasCodePath {
|
||
diagnostics.AddError(
|
||
"Некорректная конфигурация источника кода",
|
||
"Нужно указать ровно один параметр: source_dir или code_path.",
|
||
)
|
||
return false
|
||
}
|
||
|
||
return true
|
||
}
|
||
|
||
// resolveNamespace возвращает namespace ресурса или namespace из provider config.
|
||
func resolveNamespace(resourceNamespace types.String, providerNamespace string) string {
|
||
namespace := resourceNamespace.ValueString()
|
||
if namespace == "" {
|
||
namespace = providerNamespace
|
||
}
|
||
|
||
return namespace
|
||
}
|
||
|
||
// loadPackageLiteral читает bytes для literal deployment (один файл).
|
||
func loadPackageLiteral(sourceDir, codePath string) ([]byte, error) {
|
||
if sourceDir != "" {
|
||
mainFilePath, err := resolveMainSourceFile(sourceDir)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
mainFileBytes, err := os.ReadFile(mainFilePath)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("read source_dir source file %q: %w", mainFilePath, err)
|
||
}
|
||
|
||
// Node.js runtime ожидает ZIP с package.json + main.js (ESM wrapper).
|
||
if filepath.Ext(mainFilePath) == ".js" {
|
||
return buildJSDeployZip(string(mainFileBytes))
|
||
}
|
||
|
||
return mainFileBytes, nil
|
||
}
|
||
|
||
literalBytes, err := os.ReadFile(codePath)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("read code_path %q: %w", codePath, err)
|
||
}
|
||
|
||
return literalBytes, nil
|
||
}
|
||
|
||
// buildJSDeployZip wraps Node.js user code into a ZIP with package.json (ESM) + main.js wrapper.
|
||
// This matches the format expected by ghcr.io/fission/node-env runtime.
|
||
func buildJSDeployZip(code string) ([]byte, error) {
|
||
var buf bytes.Buffer
|
||
zw := zip.NewWriter(&buf)
|
||
|
||
pkgfw, err := zw.Create("package.json")
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if _, err := pkgfw.Write([]byte(`{"type":"module"}`)); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
codeJSON, err := json.Marshal(code)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("marshal user code: %w", err)
|
||
}
|
||
wrapper := fmt.Sprintf(`const __mod = { exports: {} };
|
||
(new Function('module', 'exports', %s))(__mod, __mod.exports);
|
||
const _fn = __mod.exports;
|
||
|
||
export default async function(ctx) {
|
||
const fn = typeof _fn === 'function' ? _fn : (_fn.default || _fn.handler || _fn.main);
|
||
if (!fn) throw new Error('no exported function found in user code');
|
||
const result = await fn(ctx);
|
||
if (!result) return { status: 200, body: '' };
|
||
if (typeof result.status !== 'undefined') return result;
|
||
return { status: 200, ...result };
|
||
}
|
||
`, string(codeJSON))
|
||
|
||
fw, err := zw.Create("main.js")
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if _, err := fw.Write([]byte(wrapper)); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if err := zw.Close(); err != nil {
|
||
return nil, err
|
||
}
|
||
return buf.Bytes(), nil
|
||
}
|
||
|
||
// loadPackageSourceArchive создает zip-архив из source_dir для builder pipeline.
|
||
func loadPackageSourceArchive(sourceDir string) ([]byte, error) {
|
||
var buf bytes.Buffer
|
||
zipWriter := zip.NewWriter(&buf)
|
||
|
||
err := filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if info.IsDir() {
|
||
return nil
|
||
}
|
||
|
||
relPath, err := filepath.Rel(sourceDir, path)
|
||
if err != nil {
|
||
return fmt.Errorf("compute relative path for %q: %w", path, err)
|
||
}
|
||
|
||
writer, err := zipWriter.Create(relPath)
|
||
if err != nil {
|
||
return fmt.Errorf("create zip entry %q: %w", relPath, err)
|
||
}
|
||
|
||
file, err := os.Open(path)
|
||
if err != nil {
|
||
return fmt.Errorf("open file %q: %w", path, err)
|
||
}
|
||
defer file.Close()
|
||
|
||
_, err = io.Copy(writer, file)
|
||
return err
|
||
})
|
||
if err != nil {
|
||
return nil, fmt.Errorf("zip source_dir %q: %w", sourceDir, err)
|
||
}
|
||
|
||
if err := zipWriter.Close(); err != nil {
|
||
return nil, fmt.Errorf("close zip writer: %w", err)
|
||
}
|
||
|
||
return buf.Bytes(), nil
|
||
}
|
||
|
||
// loadPackageContent загружает содержимое пакета в зависимости от deploy_type.
|
||
func loadPackageContent(sourceDir, codePath, deployType string) ([]byte, error) {
|
||
if deployType == "source" && sourceDir != "" {
|
||
return loadPackageSourceArchive(sourceDir)
|
||
}
|
||
return loadPackageLiteral(sourceDir, codePath)
|
||
}
|
||
|
||
// resolveMainSourceFile выбирает основной файл исходника из source_dir.
|
||
func resolveMainSourceFile(sourceDir string) (string, error) {
|
||
candidates := []string{"main.py", "main.js", "main.go", "main.php", "main.rb", "main.pl"}
|
||
for _, candidate := range candidates {
|
||
candidatePath := filepath.Join(sourceDir, candidate)
|
||
fileInfo, err := os.Stat(candidatePath)
|
||
if err == nil && !fileInfo.IsDir() {
|
||
return candidatePath, nil
|
||
}
|
||
}
|
||
|
||
return "", fmt.Errorf("source_dir %q must contain one of: main.py, main.js, main.go, main.php, main.rb, main.pl", sourceDir)
|
||
}
|
||
|
||
// packageToUnstructured преобразует Terraform model в Kubernetes CRD payload.
|
||
func packageToUnstructured(model packageResourceModel, namespace string, literalBytes []byte) *unstructured.Unstructured {
|
||
literalSource := base64.StdEncoding.EncodeToString(literalBytes)
|
||
|
||
deployType := "literal"
|
||
if !model.DeployType.IsNull() && !model.DeployType.IsUnknown() && model.DeployType.ValueString() != "" {
|
||
deployType = model.DeployType.ValueString()
|
||
}
|
||
|
||
spec := map[string]interface{}{
|
||
"environment": map[string]interface{}{
|
||
"name": model.Environment.ValueString(),
|
||
"namespace": namespace,
|
||
},
|
||
}
|
||
|
||
if deployType == "source" {
|
||
// Source mode: код в spec.source (для builder pipeline — Go и др.)
|
||
spec["source"] = map[string]interface{}{
|
||
"type": "literal",
|
||
"literal": literalSource,
|
||
}
|
||
} else {
|
||
// Literal/deployment mode: код в spec.deployment (Python, Node, PHP, Ruby, Perl)
|
||
spec["deployment"] = map[string]interface{}{
|
||
"type": "literal",
|
||
"literal": literalSource,
|
||
}
|
||
}
|
||
|
||
if buildCommand := model.BuildCmd.ValueString(); buildCommand != "" {
|
||
spec["buildcmd"] = buildCommand
|
||
}
|
||
|
||
object := map[string]interface{}{
|
||
"apiVersion": "fission.io/v1",
|
||
"kind": "Package",
|
||
"metadata": map[string]interface{}{
|
||
"name": model.Name.ValueString(),
|
||
"namespace": namespace,
|
||
},
|
||
"spec": spec,
|
||
}
|
||
|
||
return &unstructured.Unstructured{Object: object}
|
||
}
|
||
|
||
// unstructuredToPackageModel читает нужные поля из CRD объекта обратно в Terraform state.
|
||
func unstructuredToPackageModel(packageObject *unstructured.Unstructured, base packageResourceModel) packageResourceModel {
|
||
environmentName, _, _ := unstructured.NestedString(packageObject.Object, "spec", "environment", "name")
|
||
buildCommand, _, _ := unstructured.NestedString(packageObject.Object, "spec", "buildcmd")
|
||
buildStatus, _, _ := unstructured.NestedString(packageObject.Object, "status", "buildstatus")
|
||
buildLog, _, _ := unstructured.NestedString(packageObject.Object, "status", "buildlog")
|
||
deploymentLiteral, _, _ := unstructured.NestedString(packageObject.Object, "spec", "deployment", "literal")
|
||
sourceLiteral, _, _ := unstructured.NestedString(packageObject.Object, "spec", "source", "literal")
|
||
|
||
state := packageResourceModel{
|
||
ID: types.StringValue(fmt.Sprintf("%s/%s", packageObject.GetNamespace(), packageObject.GetName())),
|
||
Name: types.StringValue(packageObject.GetName()),
|
||
Environment: base.Environment,
|
||
SourceDir: base.SourceDir,
|
||
CodePath: base.CodePath,
|
||
CodeHash: base.CodeHash,
|
||
BuildCmd: base.BuildCmd,
|
||
DeployType: base.DeployType,
|
||
Namespace: types.StringValue(packageObject.GetNamespace()),
|
||
UID: types.StringValue(string(packageObject.GetUID())),
|
||
BuildStatus: types.StringNull(),
|
||
BuildLog: types.StringNull(),
|
||
}
|
||
|
||
if environmentName != "" {
|
||
state.Environment = types.StringValue(environmentName)
|
||
}
|
||
if buildCommand != "" {
|
||
state.BuildCmd = types.StringValue(buildCommand)
|
||
}
|
||
if buildStatus != "" {
|
||
state.BuildStatus = types.StringValue(buildStatus)
|
||
}
|
||
if buildLog != "" {
|
||
state.BuildLog = types.StringValue(buildLog)
|
||
}
|
||
|
||
// Определить hash из содержимого (deployment или source)
|
||
literalForHash := deploymentLiteral
|
||
if literalForHash == "" {
|
||
literalForHash = sourceLiteral
|
||
}
|
||
if literalForHash != "" {
|
||
if literalBytes, err := base64.StdEncoding.DecodeString(literalForHash); err == nil {
|
||
state.CodeHash = types.StringValue(calculateCodeHash(literalBytes))
|
||
}
|
||
}
|
||
|
||
return state
|
||
}
|
||
|
||
func hasManualCodeHash(codeHash types.String) bool {
|
||
return !codeHash.IsNull() && !codeHash.IsUnknown() && codeHash.ValueString() != ""
|
||
}
|
||
|
||
func calculateCodeHash(literalBytes []byte) string {
|
||
sum := sha256.Sum256(literalBytes)
|
||
return fmt.Sprintf("%x", sum)
|
||
}
|