package appfile import ( "encoding/json" "errors" "io/ioutil" "os" "path/filepath" "time" "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1" "github.com/ghodss/yaml" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2" "github.com/oam-dev/kubevela/pkg/appfile/template" cmdutil "github.com/oam-dev/kubevela/pkg/commands/util" "github.com/oam-dev/kubevela/pkg/oam" ) // error msg used in Appfile var ( ErrImageNotDefined = errors.New("image not defined") ) // DefaultAppfilePath defines the default file path that used by `vela up` command const ( DefaultJSONAppfilePath = "./vela.json" DefaultAppfilePath = "./vela.yaml" DefaultUnknowFormatAppfilePath = "./Appfile" ) // AppFile defines the spec of KubeVela Appfile type AppFile struct { Name string `json:"name"` CreateTime time.Time `json:"createTime,omitempty"` UpdateTime time.Time `json:"updateTime,omitempty"` Services map[string]Service `json:"services"` Secrets map[string]string `json:"secrets,omitempty"` configGetter configGetter } // NewAppFile init an empty AppFile struct func NewAppFile() *AppFile { return &AppFile{ Services: make(map[string]Service), Secrets: make(map[string]string), configGetter: defaultConfigGetter{}, } } // Load will load appfile from default path func Load() (*AppFile, error) { if _, err := os.Stat(DefaultAppfilePath); err == nil { return LoadFromFile(DefaultAppfilePath) } if _, err := os.Stat(DefaultJSONAppfilePath); err == nil { return LoadFromFile(DefaultJSONAppfilePath) } return LoadFromFile(DefaultUnknowFormatAppfilePath) } // JSONToYaml will convert JSON format appfile to yaml and load the AppFile struct func JSONToYaml(data []byte, appFile *AppFile) (*AppFile, error) { j, e := yaml.JSONToYAML(data) if e != nil { return nil, e } err := yaml.Unmarshal(j, appFile) if err != nil { return nil, err } return appFile, nil } // LoadFromFile will read the file and load the AppFile struct func LoadFromFile(filename string) (*AppFile, error) { b, err := ioutil.ReadFile(filepath.Clean(filename)) if err != nil { return nil, err } af := NewAppFile() // Add JSON format appfile support ext := filepath.Ext(filename) switch ext { case ".yaml", ".yml": err = yaml.Unmarshal(b, af) case ".json": af, err = JSONToYaml(b, af) default: if json.Valid(b) { af, err = JSONToYaml(b, af) } else { err = yaml.Unmarshal(b, af) } } if err != nil { return nil, err } return af, nil } // BuildOAM renders Appfile into AppConfig, Components. It also builds images for services if defined. func (app *AppFile) BuildOAM(ns string, io cmdutil.IOStreams, tm template.Manager, slience bool) ( []*v1alpha2.Component, *v1alpha2.ApplicationConfiguration, []oam.Object, error) { return app.buildOAM(ns, io, tm, slience) } func (app *AppFile) buildOAM(ns string, io cmdutil.IOStreams, tm template.Manager, silence bool) ( []*v1alpha2.Component, *v1alpha2.ApplicationConfiguration, []oam.Object, error) { appConfig := &v1alpha2.ApplicationConfiguration{ ObjectMeta: metav1.ObjectMeta{ Name: app.Name, Namespace: ns, }, } var comps []*v1alpha2.Component for sname, svc := range app.GetServices() { var image string v, ok := svc["image"] if ok { image = v.(string) } if b := svc.GetBuild(); b != nil { if image == "" { return nil, nil, nil, ErrImageNotDefined } io.Infof("\nBuilding service (%s)...\n", sname) if err := b.BuildImage(io, image); err != nil { return nil, nil, nil, err } } if !silence { io.Infof("\nRendering configs for service (%s)...\n", sname) } acComp, comp, err := svc.RenderService(tm, sname, ns, app.configGetter) if err != nil { return nil, nil, nil, err } appConfig.Spec.Components = append(appConfig.Spec.Components, *acComp) comps = append(comps, comp) } addWorkloadTypeLabel(comps, app.Services) health := addHealthScope(appConfig) return comps, appConfig, []oam.Object{health}, nil } func addWorkloadTypeLabel(comps []*v1alpha2.Component, services map[string]Service) { for _, comp := range comps { workloadType := services[comp.Name].GetType() workloadObject := comp.Spec.Workload.Object.(*unstructured.Unstructured) labels := workloadObject.GetLabels() if labels == nil { labels = map[string]string{oam.WorkloadTypeLabel: workloadType} } else { labels[oam.WorkloadTypeLabel] = workloadType } workloadObject.SetLabels(labels) } } func addHealthScope(appConfig *v1alpha2.ApplicationConfiguration) *v1alpha2.HealthScope { health := &v1alpha2.HealthScope{ TypeMeta: metav1.TypeMeta{ APIVersion: v1alpha2.HealthScopeGroupVersionKind.GroupVersion().String(), Kind: v1alpha2.HealthScopeKind, }, } health.Name = FormatDefaultHealthScopeName(appConfig.Name) health.Namespace = appConfig.Namespace health.Spec.WorkloadReferences = make([]v1alpha1.TypedReference, 0) for i := range appConfig.Spec.Components { // TODO(wonderflow): Temporarily we add health scope here, should change to use scope framework appConfig.Spec.Components[i].Scopes = append(appConfig.Spec.Components[i].Scopes, v1alpha2.ComponentScope{ ScopeReference: v1alpha1.TypedReference{ APIVersion: v1alpha2.SchemeGroupVersion.String(), Kind: v1alpha2.HealthScopeKind, Name: health.Name, }, }) } return health } // FormatDefaultHealthScopeName will create a default health scope name. func FormatDefaultHealthScopeName(appName string) string { return appName + "-default-health" }