pkg/model/history/changeset.go (144 lines of code) (raw):
// Copyright 2024 Google LLC
//
// Licensed 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 history
import (
"fmt"
"github.com/GoogleCloudPlatform/khi/pkg/common"
"github.com/GoogleCloudPlatform/khi/pkg/log"
"github.com/GoogleCloudPlatform/khi/pkg/model/enum"
"github.com/GoogleCloudPlatform/khi/pkg/model/history/resourcepath"
)
// history.ChangeSet is set of changes applicable to history.
// A parser ingest a log.LogEntry and returns a ChangeSet. ChangeSet contains multiple changes against the history.
// This change is applied atomically, when the parser returns an error, no partial changes would be written.
type ChangeSet struct {
associatedLog *log.LogEntity
revisions map[string][]*StagingResourceRevision
events map[string][]*ResourceEvent
resourceRelationshipRewrites map[string]enum.ParentRelationship
annotations []LogAnnotation
logSummaryRewrite string
logSeverityRewrite enum.Severity
aliases map[string][]string
}
func NewChangeSet(l *log.LogEntity) *ChangeSet {
return &ChangeSet{
associatedLog: l,
revisions: make(map[string][]*StagingResourceRevision),
events: make(map[string][]*ResourceEvent),
resourceRelationshipRewrites: make(map[string]enum.ParentRelationship),
logSummaryRewrite: "",
logSeverityRewrite: enum.SeverityUnknown,
annotations: []LogAnnotation{},
aliases: map[string][]string{},
}
}
func (cs *ChangeSet) RecordLogSummary(summary string) {
cs.logSummaryRewrite = summary
}
// GetLogSummary returns the summary of log to be written with the log.
func (cs *ChangeSet) GetLogSummary() string {
return cs.logSummaryRewrite
}
func (cs *ChangeSet) RecordLogSeverity(severity enum.Severity) {
cs.logSeverityRewrite = severity
}
func (cs *ChangeSet) RecordRevision(resourcePath resourcepath.ResourcePath, revision *StagingResourceRevision) {
if _, exist := cs.revisions[resourcePath.Path]; !exist {
cs.revisions[resourcePath.Path] = make([]*StagingResourceRevision, 0)
}
cs.revisions[resourcePath.Path] = append(cs.revisions[resourcePath.Path], revision)
if !revision.Inferred {
cs.annotations = append(cs.annotations, NewResourceReferenceAnnotation(resourcePath.Path))
}
cs.recordResourceRelationship(resourcePath)
}
// GetAllResourcePaths returns the all of resource paths included in this ChangeSet.
func (cs *ChangeSet) GetAllResourcePaths() []string {
paths := []string{}
for k := range cs.revisions {
paths = append(paths, k)
}
for k := range cs.events {
paths = append(paths, k)
}
return common.DedupStringArray(paths)
}
// GetRevisions returns every StagingResourceRevisions at the specified resource path.
func (cs *ChangeSet) GetRevisions(resourcePath resourcepath.ResourcePath) []*StagingResourceRevision {
if revisions, exist := cs.revisions[resourcePath.Path]; exist {
return revisions
}
return nil
}
func (cs *ChangeSet) RecordEvent(resourcePath resourcepath.ResourcePath) {
event := ResourceEvent{
Log: cs.associatedLog.ID(),
}
if _, exist := cs.events[resourcePath.Path]; !exist {
cs.events[resourcePath.Path] = make([]*ResourceEvent, 0)
}
cs.events[resourcePath.Path] = append(cs.events[resourcePath.Path], &event)
cs.annotations = append(cs.annotations, NewResourceReferenceAnnotation(resourcePath.Path))
cs.recordResourceRelationship(resourcePath)
}
// GetEvents returns every ResourceEvents at the specified resource path.
func (cs *ChangeSet) GetEvents(resourcePath resourcepath.ResourcePath) []*ResourceEvent {
if events, exist := cs.events[resourcePath.Path]; exist {
return events
}
return nil
}
func (cs *ChangeSet) RecordResourceAlias(sourceResourcePath resourcepath.ResourcePath, destResourcePath resourcepath.ResourcePath) {
if _, exist := cs.aliases[sourceResourcePath.Path]; !exist {
cs.aliases[sourceResourcePath.Path] = make([]string, 0)
}
for _, d := range cs.aliases[sourceResourcePath.Path] {
if d == destResourcePath.Path {
return
}
}
cs.aliases[sourceResourcePath.Path] = append(cs.aliases[sourceResourcePath.Path], destResourcePath.Path)
cs.recordResourceRelationship(destResourcePath)
}
func (cs *ChangeSet) recordResourceRelationship(resourcePath resourcepath.ResourcePath) error {
if lastRelationship, found := cs.resourceRelationshipRewrites[resourcePath.Path]; found {
if lastRelationship != resourcePath.ParentRelationship {
return fmt.Errorf("failed to rewrite the parentRelationship of %s. It was already rewritten to %d", resourcePath.Path, lastRelationship)
}
} else {
cs.resourceRelationshipRewrites[resourcePath.Path] = resourcePath.ParentRelationship
}
return nil
}
// FlushToHistory writes the recorded changeset to the history and returns resource paths where the resource modified.
func (cs *ChangeSet) FlushToHistory(builder *Builder) ([]string, error) {
changedPaths := []string{}
// Write revisions in this ChangeSet
for resourcePath, revisions := range cs.revisions {
tb := builder.GetTimelineBuilder(resourcePath)
for _, stagingRevision := range revisions {
revision, err := stagingRevision.commit(builder.binaryChunk, cs.associatedLog)
if err != nil {
return nil, err
}
tb.AddRevision(revision)
}
changedPaths = append(changedPaths, resourcePath)
}
// Write events in this ChangeSet
for resourcePath, events := range cs.events {
tb := builder.GetTimelineBuilder(resourcePath)
for _, event := range events {
tb.AddEvent(event)
}
changedPaths = append(changedPaths, resourcePath)
}
// Write log related properties
if cs.logSummaryRewrite != "" {
builder.setLogSummary(cs.associatedLog.ID(), cs.logSummaryRewrite)
}
if cs.logSeverityRewrite != enum.SeverityUnknown {
builder.setLogSeverity(cs.associatedLog.ID(), cs.logSeverityRewrite)
}
builder.setLogAnnotations(cs.associatedLog.ID(), cs.annotations)
// Write the alias relationships
for source, destinations := range cs.aliases {
for _, dest := range destinations {
builder.addTimelineAlias(source, dest)
}
}
// Write resource related properties
for resourcePath, relationship := range cs.resourceRelationshipRewrites {
err := builder.rewriteRelationship(resourcePath, relationship)
if err != nil {
return nil, err
}
}
return common.DedupStringArray(changedPaths), nil
}