diff --git a/bin/config.go b/bin/config.go index 44b87e529..bef436fda 100644 --- a/bin/config.go +++ b/bin/config.go @@ -347,7 +347,7 @@ func doDumpClientConfig() error { return err } - client_config := config.GetClientConfig(config_obj) + client_config := config.StripClientConfig(config_obj) res, err := yaml.Marshal(client_config) if err != nil { return fmt.Errorf("Unable to encode config: %w", err) diff --git a/bin/gui.go b/bin/gui.go index b05b5c321..cf1985dc7 100644 --- a/bin/gui.go +++ b/bin/gui.go @@ -137,7 +137,7 @@ func generateGUIConfig(datastore_directory, server_config_path, client_config_pa fd.Close() // Now also write a client config - client_config := config.GetClientConfig(config_obj) + client_config := config.StripClientConfig(config_obj) client_config.Logging = config_obj.Logging serialized, err = yaml.Marshal(client_config) diff --git a/config/client.go b/config/client.go index a93c33128..22eb26d95 100644 --- a/config/client.go +++ b/config/client.go @@ -7,9 +7,45 @@ import config_proto "www.velocidex.com/golang/velociraptor/config/proto" func GetClientConfig(config_obj *config_proto.Config) *config_proto.Config { // Copy only settings relevant to the client from the main config. client_config := &config_proto.Config{ - Version: config_obj.Version, - Client: config_obj.Client, + Version: config_obj.Version, + Client: config_obj.Client, + Autoexec: config_obj.Autoexec, + + // The below are added at runtime.. + Remappings: config_obj.Remappings, // --remap flag. + Verbose: config_obj.Verbose, // -v flag + DebugMode: config_obj.DebugMode, // --debug flag + + // Logging is populated from the client specific section. + Logging: config_obj.Client.Logging, } return client_config } + +// Produce a minima configuration for the client. This function is +// used to prepare a client config from the server config file +// (e.g. for packaging). +func StripClientConfig(config_obj *config_proto.Config) *config_proto.Config { + // Copy only settings relevant to the client from the main config. + client_config := &config_proto.Config{ + Version: config_obj.Version, + Client: config_obj.Client, + Autoexec: config_obj.Autoexec, + } + + return client_config +} + +// Determine if the binary is running as a frontend. At a minimum we +// need a valid Frontend and Datastore sections. +func IsFrontend(config_obj *config_proto.Config) bool { + return config_obj.Datastore != nil && + config_obj.Frontend != nil +} + +// Determine if the binary is running as a client. At a minimum we +// need a valid Client section. +func IsClient(config_obj *config_proto.Config) bool { + return config_obj.Client != nil +} diff --git a/config/loader.go b/config/loader.go index 0f580ad7a..c21c15ba5 100644 --- a/config/loader.go +++ b/config/loader.go @@ -106,7 +106,7 @@ func (self *Loader) WithRequiredFrontend() *Loader { self.validators = append(self.validators, validatorFunction{ name: "WithRequiredFrontend", validator: func(self *Loader, config_obj *config_proto.Config) error { // - if config_obj.Frontend == nil { + if !IsFrontend(config_obj) { return errors.New("Frontend config is required") } return nil @@ -119,7 +119,7 @@ func (self *Loader) WithRequiredClient() *Loader { self.validators = append(self.validators, validatorFunction{ name: "WithRequiredClient", validator: func(self *Loader, config_obj *config_proto.Config) error { - if config_obj.Client == nil { + if !IsClient(config_obj) { return errors.New("Client config is required") } return nil diff --git a/config/proto/config.pb.go b/config/proto/config.pb.go index 237c79c9a..c51c891ac 100644 --- a/config/proto/config.pb.go +++ b/config/proto/config.pb.go @@ -686,8 +686,12 @@ type ClientConfig struct { // system has less than 2 cores. This allows to increase the // threadhold. Set to a large number to disable. LowResourceCpuCount uint64 `protobuf:"varint,53,opt,name=low_resource_cpu_count,json=lowResourceCpuCount,proto3" json:"low_resource_cpu_count,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Client specific logging configuration - this can be different + // from the server configuration. Usually the client does not log + // anything but you can enable this for debugging. + Logging *LoggingConfig `protobuf:"bytes,55,opt,name=Logging,proto3" json:"Logging,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ClientConfig) Reset() { @@ -1035,6 +1039,13 @@ func (x *ClientConfig) GetLowResourceCpuCount() uint64 { return 0 } +func (x *ClientConfig) GetLogging() *LoggingConfig { + if x != nil { + return x.Logging + } + return nil +} + type APIConfig struct { state protoimpl.MessageState `protogen:"open.v1"` // Publicly accessible hostname. @@ -4864,7 +4875,8 @@ type Config struct { // Deprecated: Marked as deprecated in config.proto. Writeback *Writeback `protobuf:"bytes,9,opt,name=Writeback,proto3" json:"Writeback,omitempty"` // Deprecated - Mail plugin setting are now provided by args. - Mail *MailConfig `protobuf:"bytes,11,opt,name=Mail,proto3" json:"Mail,omitempty"` + Mail *MailConfig `protobuf:"bytes,11,opt,name=Mail,proto3" json:"Mail,omitempty"` + // Server specific logging configuration Logging *LoggingConfig `protobuf:"bytes,23,opt,name=Logging,proto3" json:"Logging,omitempty"` // Minion specific overrides Minion *MinionConfig `protobuf:"bytes,40,opt,name=Minion,proto3" json:"Minion,omitempty"` @@ -5186,7 +5198,7 @@ const file_config_proto_rawDesc = "" + "\tdisk_size\x18\x02 \x01(\x04BQ\xe2\xfc\xe3\xc4\x01K\x12IHow many bytes to store in the ring buffer on disk (0 mean no disk file).R\bdiskSize\x12x\n" + "\x0efilename_linux\x18\x04 \x01(\tBQ\xe2\xfc\xe3\xc4\x01K\x12IName of file to store the ring buffer in (if empty we do not use a file).R\rfilenameLinux\x12|\n" + "\x10filename_windows\x18\x05 \x01(\tBQ\xe2\xfc\xe3\xc4\x01K\x12IName of file to store the ring buffer in (if empty we do not use a file).R\x0ffilenameWindows\x12z\n" + - "\x0ffilename_darwin\x18\x06 \x01(\tBQ\xe2\xfc\xe3\xc4\x01K\x12IName of file to store the ring buffer in (if empty we do not use a file).R\x0efilenameDarwin\"\xc1\x1c\n" + + "\x0ffilename_darwin\x18\x06 \x01(\tBQ\xe2\xfc\xe3\xc4\x01K\x12IName of file to store the ring buffer in (if empty we do not use a file).R\x0efilenameDarwin\"\xf1\x1c\n" + "\fClientConfig\x12\x80\x01\n" + "\x06labels\x18\x06 \x03(\tBh\xe2\xfc\xe3\xc4\x01b\x12`A list of labels the client has. This allows selected groups of clients to be targeted in hunts.R\x06labels\x12a\n" + "\vserver_urls\x18\b \x03(\tB@\xe2\xfc\xe3\xc4\x01:\x128A list of server URLs the client will try to connect to.R\n" + @@ -5236,7 +5248,8 @@ const file_config_proto_rawDesc = "" + "panic_file\x181 \x01(\tR\tpanicFile\x12=\n" + "\x1binsecure_network_trace_file\x183 \x01(\tR\x18insecureNetworkTraceFile\x12/\n" + "\x14low_resource_max_cpu\x184 \x01(\x04R\x11lowResourceMaxCpu\x123\n" + - "\x16low_resource_cpu_count\x185 \x01(\x04R\x13lowResourceCpuCount\x1aD\n" + + "\x16low_resource_cpu_count\x185 \x01(\x04R\x13lowResourceCpuCount\x12.\n" + + "\aLogging\x187 \x01(\v2\x14.proto.LoggingConfigR\aLogging\x1aD\n" + "\x16FallbackAddressesEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xad\x04\n" + @@ -5718,51 +5731,52 @@ var file_config_proto_depIdxs = []int32{ 6, // 7: proto.ClientConfig.local_buffer:type_name -> proto.RingBufferConfig 31, // 8: proto.ClientConfig.Crypto:type_name -> proto.CryptoConfig 36, // 9: proto.ClientConfig.fallback_addresses:type_name -> proto.ClientConfig.FallbackAddressesEntry - 37, // 10: proto.ProxyConfig.proxy_url_regexp:type_name -> proto.ProxyConfig.ProxyUrlRegexpEntry - 38, // 11: proto.OIDCClaims.role_map:type_name -> proto.OIDCClaims.RoleMapEntry - 39, // 12: proto.Authenticator.oidc_auth_url_params:type_name -> proto.Authenticator.OidcAuthUrlParamsEntry - 13, // 13: proto.Authenticator.claims:type_name -> proto.OIDCClaims - 14, // 14: proto.Authenticator.sub_authenticators:type_name -> proto.Authenticator - 18, // 15: proto.GUIConfig.reverse_proxy:type_name -> proto.ReverseProxyConfig - 11, // 16: proto.GUIConfig.links:type_name -> proto.GUILink - 16, // 17: proto.GUIConfig.initial_users:type_name -> proto.GUIUser - 3, // 18: proto.GUIConfig.initial_orgs:type_name -> proto.InitialOrgRecord - 14, // 19: proto.GUIConfig.authenticator:type_name -> proto.Authenticator - 10, // 20: proto.FrontendConfig.proxy_config:type_name -> proto.ProxyConfig - 19, // 21: proto.FrontendConfig.dyn_dns:type_name -> proto.DynDNSConfig - 20, // 22: proto.FrontendConfig.resources:type_name -> proto.FrontendResourceControl - 25, // 23: proto.LoggingConfig.debug:type_name -> proto.LoggingRetentionConfig - 25, // 24: proto.LoggingConfig.info:type_name -> proto.LoggingRetentionConfig - 25, // 25: proto.LoggingConfig.error:type_name -> proto.LoggingRetentionConfig - 41, // 26: proto.AutoExecConfig.artifact_definitions:type_name -> proto.Artifact - 32, // 27: proto.RemappingConfig.from:type_name -> proto.MountPoint - 32, // 28: proto.RemappingConfig.on:type_name -> proto.MountPoint - 42, // 29: proto.RemappingConfig.env:type_name -> proto.VQLEnv - 0, // 30: proto.Config.version:type_name -> proto.Version - 7, // 31: proto.Config.Client:type_name -> proto.ClientConfig - 8, // 32: proto.Config.API:type_name -> proto.APIConfig - 15, // 33: proto.Config.GUI:type_name -> proto.GUIConfig - 17, // 34: proto.Config.CA:type_name -> proto.CAConfig - 21, // 35: proto.Config.Frontend:type_name -> proto.FrontendConfig - 21, // 36: proto.Config.ExtraFrontends:type_name -> proto.FrontendConfig - 22, // 37: proto.Config.Datastore:type_name -> proto.DatastoreConfig - 2, // 38: proto.Config.Writeback:type_name -> proto.Writeback - 24, // 39: proto.Config.Mail:type_name -> proto.MailConfig - 26, // 40: proto.Config.Logging:type_name -> proto.LoggingConfig - 23, // 41: proto.Config.Minion:type_name -> proto.MinionConfig - 27, // 42: proto.Config.Monitoring:type_name -> proto.MonitoringConfig - 9, // 43: proto.Config.api_config:type_name -> proto.ApiClientConfig - 28, // 44: proto.Config.autoexec:type_name -> proto.AutoExecConfig - 30, // 45: proto.Config.defaults:type_name -> proto.Defaults - 33, // 46: proto.Config.remappings:type_name -> proto.RemappingConfig - 29, // 47: proto.Config.services:type_name -> proto.ServerServicesConfig - 34, // 48: proto.Config.security:type_name -> proto.Security - 12, // 49: proto.OIDCClaims.RoleMapEntry.value:type_name -> proto.OIDCACL - 50, // [50:50] is the sub-list for method output_type - 50, // [50:50] is the sub-list for method input_type - 50, // [50:50] is the sub-list for extension type_name - 50, // [50:50] is the sub-list for extension extendee - 0, // [0:50] is the sub-list for field type_name + 26, // 10: proto.ClientConfig.Logging:type_name -> proto.LoggingConfig + 37, // 11: proto.ProxyConfig.proxy_url_regexp:type_name -> proto.ProxyConfig.ProxyUrlRegexpEntry + 38, // 12: proto.OIDCClaims.role_map:type_name -> proto.OIDCClaims.RoleMapEntry + 39, // 13: proto.Authenticator.oidc_auth_url_params:type_name -> proto.Authenticator.OidcAuthUrlParamsEntry + 13, // 14: proto.Authenticator.claims:type_name -> proto.OIDCClaims + 14, // 15: proto.Authenticator.sub_authenticators:type_name -> proto.Authenticator + 18, // 16: proto.GUIConfig.reverse_proxy:type_name -> proto.ReverseProxyConfig + 11, // 17: proto.GUIConfig.links:type_name -> proto.GUILink + 16, // 18: proto.GUIConfig.initial_users:type_name -> proto.GUIUser + 3, // 19: proto.GUIConfig.initial_orgs:type_name -> proto.InitialOrgRecord + 14, // 20: proto.GUIConfig.authenticator:type_name -> proto.Authenticator + 10, // 21: proto.FrontendConfig.proxy_config:type_name -> proto.ProxyConfig + 19, // 22: proto.FrontendConfig.dyn_dns:type_name -> proto.DynDNSConfig + 20, // 23: proto.FrontendConfig.resources:type_name -> proto.FrontendResourceControl + 25, // 24: proto.LoggingConfig.debug:type_name -> proto.LoggingRetentionConfig + 25, // 25: proto.LoggingConfig.info:type_name -> proto.LoggingRetentionConfig + 25, // 26: proto.LoggingConfig.error:type_name -> proto.LoggingRetentionConfig + 41, // 27: proto.AutoExecConfig.artifact_definitions:type_name -> proto.Artifact + 32, // 28: proto.RemappingConfig.from:type_name -> proto.MountPoint + 32, // 29: proto.RemappingConfig.on:type_name -> proto.MountPoint + 42, // 30: proto.RemappingConfig.env:type_name -> proto.VQLEnv + 0, // 31: proto.Config.version:type_name -> proto.Version + 7, // 32: proto.Config.Client:type_name -> proto.ClientConfig + 8, // 33: proto.Config.API:type_name -> proto.APIConfig + 15, // 34: proto.Config.GUI:type_name -> proto.GUIConfig + 17, // 35: proto.Config.CA:type_name -> proto.CAConfig + 21, // 36: proto.Config.Frontend:type_name -> proto.FrontendConfig + 21, // 37: proto.Config.ExtraFrontends:type_name -> proto.FrontendConfig + 22, // 38: proto.Config.Datastore:type_name -> proto.DatastoreConfig + 2, // 39: proto.Config.Writeback:type_name -> proto.Writeback + 24, // 40: proto.Config.Mail:type_name -> proto.MailConfig + 26, // 41: proto.Config.Logging:type_name -> proto.LoggingConfig + 23, // 42: proto.Config.Minion:type_name -> proto.MinionConfig + 27, // 43: proto.Config.Monitoring:type_name -> proto.MonitoringConfig + 9, // 44: proto.Config.api_config:type_name -> proto.ApiClientConfig + 28, // 45: proto.Config.autoexec:type_name -> proto.AutoExecConfig + 30, // 46: proto.Config.defaults:type_name -> proto.Defaults + 33, // 47: proto.Config.remappings:type_name -> proto.RemappingConfig + 29, // 48: proto.Config.services:type_name -> proto.ServerServicesConfig + 34, // 49: proto.Config.security:type_name -> proto.Security + 12, // 50: proto.OIDCClaims.RoleMapEntry.value:type_name -> proto.OIDCACL + 51, // [51:51] is the sub-list for method output_type + 51, // [51:51] is the sub-list for method input_type + 51, // [51:51] is the sub-list for extension type_name + 51, // [51:51] is the sub-list for extension extendee + 0, // [0:51] is the sub-list for field type_name } func init() { file_config_proto_init() } diff --git a/config/proto/config.proto b/config/proto/config.proto index 41b252ff2..8460400ec 100644 --- a/config/proto/config.proto +++ b/config/proto/config.proto @@ -353,6 +353,12 @@ message ClientConfig { // system has less than 2 cores. This allows to increase the // threadhold. Set to a large number to disable. uint64 low_resource_cpu_count = 53; + + + // Client specific logging configuration - this can be different + // from the server configuration. Usually the client does not log + // anything but you can enable this for debugging. + LoggingConfig Logging = 55; } message APIConfig { @@ -1549,6 +1555,7 @@ message Config { // Deprecated - Mail plugin setting are now provided by args. MailConfig Mail = 11; + // Server specific logging configuration LoggingConfig Logging = 23; // Minion specific overrides diff --git a/logging/logging.go b/logging/logging.go index 4c37fc977..6f3ce99e7 100644 --- a/logging/logging.go +++ b/logging/logging.go @@ -102,9 +102,11 @@ func InitLogging(config_obj *config_proto.Config) error { &GenericComponent, &FrontendComponent, &ClientComponent, &GUIComponent, &ToolComponent, &APICmponent, &Audit} + logging_config := getLoggingConfig(config_obj) + // User asked for all components to go in the same log. - if config_obj.Logging != nil && - !config_obj.Logging.SeparateLogsPerComponent { + if logging_config != nil && + !logging_config.SeparateLogsPerComponent { components = []*string{&GenericComponent} } @@ -303,8 +305,9 @@ func (self *LogManager) GetLogger( self.mu.Lock() defer self.mu.Unlock() - if config_obj.Logging != nil && - !config_obj.Logging.SeparateLogsPerComponent { + logging_config := getLoggingConfig(config_obj) + if logging_config != nil && + !logging_config.SeparateLogsPerComponent { component = &GenericComponent } @@ -345,10 +348,12 @@ func getRotator( rotator_config *config_proto.LoggingRetentionConfig, base_path string) (io.Writer, error, bool) { + logging_config := getLoggingConfig(config_obj) + if rotator_config == nil { rotator_config = &config_proto.LoggingRetentionConfig{ - RotationTime: config_obj.Logging.RotationTime, - MaxAge: config_obj.Logging.MaxAge, + RotationTime: logging_config.RotationTime, + MaxAge: logging_config.MaxAge, } } @@ -399,12 +404,14 @@ func (self *LogManager) makeNewComponent( DisableHTMLEscape: true, } + logging_config := getLoggingConfig(config_obj) + if !disable_log_to_files && config_obj != nil && - config_obj.Logging != nil && - config_obj.Logging.OutputDirectory != "" { + logging_config != nil && + logging_config.OutputDirectory != "" { - output_directory := utils.ExpandEnv(config_obj.Logging.OutputDirectory) + output_directory := utils.ExpandEnv(logging_config.OutputDirectory) base_directory := utils.Join(output_directory, node_name) err := os.MkdirAll(base_directory, 0700) if err != nil { @@ -417,7 +424,7 @@ func (self *LogManager) makeNewComponent( Prelog("Initializing logging for %v\n", base_filename) rotator, err, enable := getRotator( - config_obj, config_obj.Logging.Debug, + config_obj, logging_config.Debug, base_filename+"_debug.log") if err != nil { return nil, err @@ -426,7 +433,7 @@ func (self *LogManager) makeNewComponent( enabled[DEBUG] = enable rotator, err, enable = getRotator( - config_obj, config_obj.Logging.Info, + config_obj, logging_config.Info, base_filename+"_info.log") if err != nil { return nil, err @@ -435,7 +442,7 @@ func (self *LogManager) makeNewComponent( enabled[INFO] = enable rotator, err, enable = getRotator( - config_obj, config_obj.Logging.Error, + config_obj, logging_config.Error, base_filename+"_error.log") if err != nil { return nil, err @@ -584,3 +591,11 @@ func (self inMemoryLogWriter) Write(p []byte) (n int, err error) { func newInMemoryLogWriter() *inMemoryLogWriter { return &inMemoryLogWriter{} } + +func getLoggingConfig(config_obj *config_proto.Config) *config_proto.LoggingConfig { + logging_config := config_obj.Logging + if logging_config == nil && config_obj.Client != nil { + logging_config = config_obj.Client.Logging + } + return logging_config +} diff --git a/logging/syslog.go b/logging/syslog.go index fd5f46891..768aa9645 100644 --- a/logging/syslog.go +++ b/logging/syslog.go @@ -14,24 +14,26 @@ func maybeAddRemoteSyslog( ctx context.Context, config_obj *config_proto.Config, manager *LogManager) error { - if config_obj.Logging == nil || - config_obj.Logging.RemoteSyslogServer == "" { + logging_config := getLoggingConfig(config_obj) + + if logging_config == nil || + logging_config.RemoteSyslogServer == "" { return nil } - protocol := config_obj.Logging.RemoteSyslogProtocol + protocol := logging_config.RemoteSyslogProtocol if protocol == "" { protocol = "udp" } - server := config_obj.Logging.RemoteSyslogServer + server := logging_config.RemoteSyslogServer if !strings.Contains(server, ":") { server += ":514" } components := make(map[*string]bool) - for _, c := range config_obj.Logging.RemoteSyslogComponents { + for _, c := range logging_config.RemoteSyslogComponents { switch c { case GenericComponent: components[&GenericComponent] = true diff --git a/vql/tools/packaging/debian.go b/vql/tools/packaging/debian.go index a30045e63..57715173b 100644 --- a/vql/tools/packaging/debian.go +++ b/vql/tools/packaging/debian.go @@ -65,6 +65,10 @@ type DEBBuilder struct { state *ordereddict.Dict } +func (self *DEBBuilder) Close() error { + return self.DebPkg.Close() +} + func (self *DEBBuilder) AddFileString(data, path string) error { switch path { diff --git a/vql/tools/packaging/debian_test.go b/vql/tools/packaging/debian_test.go index 55f8b08cd..f56191427 100644 --- a/vql/tools/packaging/debian_test.go +++ b/vql/tools/packaging/debian_test.go @@ -20,6 +20,7 @@ func (self *PackagingTestSuite) TestDEBClient() { builder, err := BuildDeb(spec) assert.NoError(self.T(), err) + defer builder.Close() goldie.Assert(self.T(), "TestDEBClient", []byte(builder.Debug())) @@ -36,6 +37,7 @@ func (self *PackagingTestSuite) TestDEBServer() { builder, err := BuildDeb(spec) assert.NoError(self.T(), err) + defer builder.Close() goldie.Assert(self.T(), "TestDEBServer", []byte(builder.Debug())) @@ -57,6 +59,7 @@ func (self *PackagingTestSuite) TestDEBServerMaster() { builder, err := BuildDeb(spec) assert.NoError(self.T(), err) + defer builder.Close() goldie.Assert(self.T(), "TestDEBServerMaster", []byte(builder.Debug())) @@ -79,6 +82,7 @@ func (self *PackagingTestSuite) TestDEBServerMinion() { builder, err := BuildDeb(spec) assert.NoError(self.T(), err) + defer builder.Close() goldie.Assert(self.T(), "TestDEBServerMinion", []byte(builder.Debug())) diff --git a/vql/tools/packaging/package.go b/vql/tools/packaging/package.go index 6c634c9ce..82934e2e0 100644 --- a/vql/tools/packaging/package.go +++ b/vql/tools/packaging/package.go @@ -243,6 +243,7 @@ func (self CreatePackagePlugin) Call(ctx context.Context, scope.Log("ERROR:%v:Unable to create RPM: %w", self.name, err) return } + defer builder.Close() filename := utils.Join(arg.DirName, package_spec.OutputFilename()) scope.Log("DEBUG:%v: writing file %v", self.name, filename) @@ -336,7 +337,7 @@ func validateClientConfig( // Derive client config from server config if config_yaml == "" { - return config.GetClientConfig(config_obj), nil + return config.StripClientConfig(config_obj), nil } // Load and validate the client config file. @@ -350,7 +351,7 @@ func validateClientConfig( // Strip any unrelated fields for the client config (in case the // user passed a server config accidentally). - return config.GetClientConfig(client_config), nil + return config.StripClientConfig(client_config), nil } func (self CreatePackagePlugin) Info(scope vfilter.Scope, type_map *vfilter.TypeMap) *vfilter.PluginInfo { diff --git a/vql/tools/packaging/rpm.go b/vql/tools/packaging/rpm.go index 7888f4e2e..e67d0bf34 100644 --- a/vql/tools/packaging/rpm.go +++ b/vql/tools/packaging/rpm.go @@ -62,6 +62,10 @@ type RPMBuilder struct { pack *rpmpack.RPM } +func (self *RPMBuilder) Close() error { + return nil +} + func (self *RPMBuilder) AddFile(f rpmpack.RPMFile) { switch f.Name { case "Preun": @@ -203,4 +207,5 @@ func BuildRPM(spec *PackageSpec) (Builder, error) { type Builder interface { Bytes(scope vfilter.Scope) ([]byte, error) Debug() string + Close() error }