Repository: GoogleCloudPlatform/osconfig
Pull request #825: Refactor /inventory with dependency injection
Author: login: burov
Labels: name: size/L
name: approved
name: lgtm
Created: 2025-05-07T15:25:11Z | Merged: 2025-05-08T14:40:40Z



Diff:
diff --git a/agentendpoint/agentendpoint.go b/agentendpoint/agentendpoint.go
index b95f32a88..cb18b9f30 100644
--- a/agentendpoint/agentendpoint.go
+++ b/agentendpoint/agentendpoint.go
@@ -27,6 +27,7 @@ import (
 	agentendpoint "cloud.google.com/go/osconfig/agentendpoint/apiv1"
 	"github.com/GoogleCloudPlatform/osconfig/agentconfig"
 	"github.com/GoogleCloudPlatform/osconfig/clog"
+	"github.com/GoogleCloudPlatform/osconfig/inventory"
 	"github.com/GoogleCloudPlatform/osconfig/osinfo"
 	"github.com/GoogleCloudPlatform/osconfig/retryutil"
 	"github.com/GoogleCloudPlatform/osconfig/tasker"
@@ -58,6 +59,8 @@ type Client struct {
 	noti   chan struct{}
 	closed bool
 	mx     sync.Mutex
+
+	inventoryProvider inventory.Provider
 }
 
 // NewClient a new agentendpoint Client.
@@ -83,7 +86,11 @@ func NewClient(ctx context.Context) (*Client, error) {
 		return nil, err
 	}
 
-	return &Client{raw: c, noti: make(chan struct{}, 1)}, nil
+	return &Client{
+		raw:               c,
+		noti:              make(chan struct{}, 1),
+		inventoryProvider: inventory.NewProvider(),
+	}, nil
 }
 
 // Close cancels WaitForTaskNotification and closes the underlying ClientConn.
@@ -109,7 +116,7 @@ func (c *Client) RegisterAgent(ctx context.Context) error {
 		return err
 	}
 
-	oi := &osinfo.OSInfo{}
+	oi := osinfo.OSInfo{}
 	if agentconfig.OSInventoryEnabled() {
 		oi, err = osinfo.Get()
 		if err != nil {
diff --git a/agentendpoint/inventory.go b/agentendpoint/inventory.go
index 78a2fedd8..333700537 100644
--- a/agentendpoint/inventory.go
+++ b/agentendpoint/inventory.go
@@ -31,7 +31,7 @@ const (
 
 // ReportInventory writes inventory to guest attributes and reports it to agent endpoint.
 func (c *Client) ReportInventory(ctx context.Context) {
-	state := inventory.Get(ctx)
+	state := c.inventoryProvider.Get(ctx)
 
 	if agentconfig.GuestAttributesEnabled() && !agentconfig.DisableInventoryWrite() {
 		clog.Infof(ctx, "Writing inventory to guest attributes")
diff --git a/inventory/inventory.go b/inventory/inventory.go
index 8ed64f53b..97c4f6717 100644
--- a/inventory/inventory.go
+++ b/inventory/inventory.go
@@ -41,21 +41,59 @@ type InstanceInventory struct {
 	LastUpdated          string
 }
 
-// Get generates inventory data.
-func Get(ctx context.Context) *InstanceInventory {
+type clock interface {
+	Now() time.Time
+}
+
+type defaultClock struct{}
+
+func newDefaultClock() clock {
+	return defaultClock{}
+}
+
+func (dc defaultClock) Now() time.Time {
+	return time.Now()
+}
+
+// Provider extract all inventormation and returns InstanceInventory aggregate
+type Provider interface {
+	Get(context.Context) *InstanceInventory
+}
+
+type defaultInventoryProvider struct {
+	osInfoProvider osinfo.Provider
+
+	packageUpdatesProvider    packages.PackageUpdatesProvider
+	installedPackagesProvider packages.InstalledPackagesProvider
+
+	clock clock
+}
+
+// NewProvider returns ready to work default provider
+func NewProvider() Provider {
+	return &defaultInventoryProvider{
+		osInfoProvider:            osinfo.NewProvider(),
+		packageUpdatesProvider:    packages.NewPackageUpdatesProvider(),
+		installedPackagesProvider: packages.NewInstalledPackagesProvider(),
+		clock:                     newDefaultClock(),
+	}
+}
+
+// Get extracts all required data from the VM and returns it as InstanceInventory aggregate
+func (p *defaultInventoryProvider) Get(ctx context.Context) *InstanceInventory {
 	clog.Debugf(ctx, "Gathering instance inventory.")
 
-	installedPackages, err := packages.GetInstalledPackages(ctx)
+	installedPackages, err := p.installedPackagesProvider.GetInstalledPackages(ctx)
 	if err != nil {
 		clog.Errorf(ctx, "packages.GetInstalledPackages() error: %v", err)
 	}
 
-	packageUpdates, err := packages.GetPackageUpdates(ctx)
+	packageUpdates, err := p.packageUpdatesProvider.GetPackageUpdates(ctx)
 	if err != nil {
 		clog.Errorf(ctx, "packages.GetPackageUpdates() error: %v", err)
 	}
 
-	oi, err := osinfo.Get()
+	oi, err := p.osInfoProvider.GetOSInfo(ctx)
 	if err != nil {
 		clog.Errorf(ctx, "osinfo.Get() error: %v", err)
 	}
@@ -69,8 +107,8 @@ func Get(ctx context.Context) *InstanceInventory {
 		KernelRelease:        oi.KernelRelease,
 		Architecture:         oi.Architecture,
 		OSConfigAgentVersion: agentconfig.Version(),
-		InstalledPackages:    installedPackages,
-		PackageUpdates:       packageUpdates,
-		LastUpdated:          time.Now().UTC().Format(time.RFC3339),
+		InstalledPackages:    &installedPackages,
+		PackageUpdates:       &packageUpdates,
+		LastUpdated:          p.clock.Now().UTC().Format(time.RFC3339),
 	}
 }
diff --git a/inventory/inventory_test.go b/inventory/inventory_test.go
new file mode 100644
index 000000000..3bbd9f251
--- /dev/null
+++ b/inventory/inventory_test.go
@@ -0,0 +1,172 @@
+package inventory
+
+import (
+	"context"
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/GoogleCloudPlatform/osconfig/osinfo"
+	"github.com/GoogleCloudPlatform/osconfig/packages"
+	"github.com/google/go-cmp/cmp"
+)
+
+func TestProvider(t *testing.T) {
+	osInfo := osinfo.OSInfo{
+		Hostname:      "testhost",
+		LongName:      "testLong",
+		ShortName:     "testShort",
+		Version:       "testVersion",
+		KernelVersion: "#1 SMP PREEMPT_DYNAMIC Debian 6.1.123-1 (2025-01-02)",
+		KernelRelease: "6.1.0-29-cloud-amd64",
+		Architecture:  "x86_64",
+	}
+
+	updates := packages.Packages{
+		Yum: []*packages.PkgInfo{{Name: "YumPkgUpdate", Arch: "Arch", Version: "Version"}},
+		Apt: []*packages.PkgInfo{{Name: "AptPkgUpdate", Arch: "Arch", Version: "Version"}},
+	}
+
+	installed := packages.Packages{
+		Yum:    []*packages.PkgInfo{{Name: "YumInstalledPkg", Arch: "Arch", Version: "Version"}},
+		GooGet: []*packages.PkgInfo{{Name: "GooGetInstalledPkg", Arch: "Arch", Version: "Version"}},
+	}
+
+	tests := []struct {
+		name string
+		stub *stubProvider
+		want *InstanceInventory
+	}{
+		{
+			name: "all providers failed, returns empty result",
+			stub: &stubProvider{
+				osinfo: func(_ context.Context) (osinfo.OSInfo, error) { return osinfo.OSInfo{}, fmt.Errorf("unexpected error") },
+				packageUpdates: func(_ context.Context) (packages.Packages, error) {
+					return packages.Packages{}, fmt.Errorf("unexpected error")
+				},
+				installedPackages: func(_ context.Context) (packages.Packages, error) {
+					return packages.Packages{}, fmt.Errorf("unexpected error")
+				},
+			},
+			want: &InstanceInventory{
+				InstalledPackages: &packages.Packages{},
+				PackageUpdates:    &packages.Packages{},
+				LastUpdated:       "1970-01-01T10:00:00Z",
+			},
+		},
+		{
+			name: "all providers succeeded, returns all data",
+			stub: &stubProvider{
+				osinfo: func(_ context.Context) (osinfo.OSInfo, error) { return osInfo, nil },
+				packageUpdates: func(_ context.Context) (packages.Packages, error) {
+					return updates, nil
+				},
+				installedPackages: func(_ context.Context) (packages.Packages, error) {
+					return installed, nil
+				},
+			},
+
+			want: &InstanceInventory{
+				Hostname:             "testhost",
+				LongName:             "testLong",
+				ShortName:            "testShort",
+				Version:              "testVersion",
+				Architecture:         "x86_64",
+				KernelVersion:        "#1 SMP PREEMPT_DYNAMIC Debian 6.1.123-1 (2025-01-02)",
+				KernelRelease:        "6.1.0-29-cloud-amd64",
+				OSConfigAgentVersion: "",
+				InstalledPackages: &packages.Packages{
+					Yum:    []*packages.PkgInfo{{Name: "YumInstalledPkg", Arch: "Arch", Version: "Version"}},
+					GooGet: []*packages.PkgInfo{{Name: "GooGetInstalledPkg", Arch: "Arch", Version: "Version"}},
+				},
+				PackageUpdates: &packages.Packages{
+					Yum: []*packages.PkgInfo{{Name: "YumPkgUpdate", Arch: "Arch", Version: "Version"}},
+					Apt: []*packages.PkgInfo{{Name: "AptPkgUpdate", Arch: "Arch", Version: "Version"}},
+				},
+				LastUpdated: "1970-01-01T10:00:00Z",
+			},
+		},
+		{
+			name: "some providers succeeded, returns available data",
+			stub: &stubProvider{
+				osinfo: func(_ context.Context) (osinfo.OSInfo, error) { return osInfo, nil },
+				packageUpdates: func(_ context.Context) (packages.Packages, error) {
+					return packages.Packages{}, fmt.Errorf("unexpected error")
+				},
+				installedPackages: func(_ context.Context) (packages.Packages, error) {
+					return installed, nil
+				},
+			},
+
+			want: &InstanceInventory{
+				Hostname:             "testhost",
+				LongName:             "testLong",
+				ShortName:            "testShort",
+				Version:              "testVersion",
+				Architecture:         "x86_64",
+				KernelVersion:        "#1 SMP PREEMPT_DYNAMIC Debian 6.1.123-1 (2025-01-02)",
+				KernelRelease:        "6.1.0-29-cloud-amd64",
+				OSConfigAgentVersion: "",
+				PackageUpdates:       &packages.Packages{},
+				InstalledPackages: &packages.Packages{
+					Yum:    []*packages.PkgInfo{{Name: "YumInstalledPkg", Arch: "Arch", Version: "Version"}},
+					GooGet: []*packages.PkgInfo{{Name: "GooGetInstalledPkg", Arch: "Arch", Version: "Version"}},
+				},
+				LastUpdated: "1970-01-01T10:00:00Z",
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			provider := defaultInventoryProvider{
+				osInfoProvider:            tt.stub,
+				packageUpdatesProvider:    tt.stub,
+				installedPackagesProvider: tt.stub,
+				clock:                     stubClock{},
+			}
+
+			ctx := context.Background()
+			got := provider.Get(ctx)
+
+			if diff := cmp.Diff(tt.want, got); diff != "" {
+				t.Errorf("unexpected diff, diff:\n%s", diff)
+			}
+
+		})
+	}
+
+}
+
+func TestNewProvider(t *testing.T) {
+	provider := NewProvider()
+
+	if provider == nil {
+		t.Errorf("provider is not valid")
+
+	}
+}
+
+type stubClock struct{}
+
+func (sc stubClock) Now() time.Time {
+	return time.UnixMicro(0).Add(10 * time.Hour)
+}
+
+type stubProvider struct {
+	osinfo            func(context.Context) (osinfo.OSInfo, error)
+	packageUpdates    func(context.Context) (packages.Packages, error)
+	installedPackages func(context.Context) (packages.Packages, error)
+}
+
+func (p stubProvider) GetOSInfo(ctx context.Context) (osinfo.OSInfo, error) {
+	return p.osinfo(ctx)
+}
+
+func (p stubProvider) GetInstalledPackages(ctx context.Context) (packages.Packages, error) {
+	return p.installedPackages(ctx)
+}
+
+func (p stubProvider) GetPackageUpdates(ctx context.Context) (packages.Packages, error) {
+	return p.packageUpdates(ctx)
+}
diff --git a/osinfo/osinfo.go b/osinfo/osinfo.go
index 19336d209..b11a52b4f 100644
--- a/osinfo/osinfo.go
+++ b/osinfo/osinfo.go
@@ -28,7 +28,24 @@ const (
 
 // Provider is an interface for OSInfo extraction on different systems.
 type Provider interface {
-	Get(context.Context) (OSInfo, error)
+	GetOSInfo(context.Context) (OSInfo, error)
+}
+
+// NewProvider returns fully function provider.
+func NewProvider() Provider {
+	return defaultProvider{}
+}
+
+type defaultProvider struct{}
+
+// GetOSInfo extract return OSInfo for current platform.
+func (defaultProvider) GetOSInfo(ctx context.Context) (OSInfo, error) {
+	osinfo, err := Get()
+	if err != nil {
+		return OSInfo{}, err
+	}
+
+	return osinfo, nil
 }
 
 // OSInfo describes an operating system.
diff --git a/osinfo/osinfo_linux.go b/osinfo/osinfo_linux.go
index 701e85da3..fe69125d4 100644
--- a/osinfo/osinfo_linux.go
+++ b/osinfo/osinfo_linux.go
@@ -39,7 +39,7 @@ var (
 )
 
 // Get reports OSInfo.
-func Get() (*OSInfo, error) {
+func Get() (OSInfo, error) {
 	// Eventually we will get rid of this function and will use providers directly
 	// Providers should support context to be able to handle cancelation and logging
 	// so far we just create empty context to connect the API.
@@ -47,15 +47,15 @@ func Get() (*OSInfo, error) {
 
 	osInfoProvider, err := NewLinuxOsInfoProvider(getOsNameAndVersionProvider(ctx))
 	if err != nil {
-		return nil, fmt.Errorf("unable to extract osinfo, err:  %w", err)
+		return OSInfo{}, fmt.Errorf("unable to extract osinfo, err:  %w", err)
 	}
 
-	osInfo, err := osInfoProvider.Get(ctx)
+	osInfo, err := osInfoProvider.GetOSInfo(ctx)
 	if err != nil {
-		return &osInfo, err
+		return osInfo, err
 	}
 
-	return &osInfo, nil
+	return osInfo, nil
 }
 
 // LinuxOsInfoProvider is a provider of OSInfo for the linux based systems.
@@ -77,8 +77,8 @@ func NewLinuxOsInfoProvider(nameAndVersionProvider osNameAndVersionProvider) (*L
 	}, nil
 }
 
-// Get gather all required information and returns OSInfo.
-func (oip *LinuxOsInfoProvider) Get(ctx context.Context) (OSInfo, error) {
+// GetOSInfo gather all required information and returns OSInfo.
+func (oip *LinuxOsInfoProvider) GetOSInfo(ctx context.Context) (OSInfo, error) {
 	short, long, version := oip.nameAndVersionProvider()
 
 	return OSInfo{
diff --git a/osinfo/osinfo_linux_test.go b/osinfo/osinfo_linux_test.go
index ed4e46ff2..5a5e2b664 100644
--- a/osinfo/osinfo_linux_test.go
+++ b/osinfo/osinfo_linux_test.go
@@ -51,7 +51,7 @@ func TestLinuxOsInfoProvider(t *testing.T) {
 	}
 
 	ctx := context.Background()
-	osInfo, err := linuxOsinfoProvider.Get(ctx)
+	osInfo, err := linuxOsinfoProvider.GetOSInfo(ctx)
 	if err != nil {
 		t.Errorf("Unexpected error, err: %v", err)
 	}
diff --git a/osinfo/osinfo_windows.go b/osinfo/osinfo_windows.go
index 48d5b687f..5a2a2bffd 100644
--- a/osinfo/osinfo_windows.go
+++ b/osinfo/osinfo_windows.go
@@ -138,8 +138,8 @@ type win32OperatingSystem struct {
 }
 
 // Get reports OSInfo.
-func Get() (*OSInfo, error) {
-	oi := &OSInfo{ShortName: DefaultShortNameWindows, Architecture: NormalizeArchitecture(runtime.GOARCH)}
+func Get() (OSInfo, error) {
+	oi := OSInfo{ShortName: DefaultShortNameWindows, Architecture: NormalizeArchitecture(runtime.GOARCH)}
 
 	hn, err := os.Hostname()
 	if err != nil {
diff --git a/packages/packages.go b/packages/packages.go
index 8a030f6a0..a34dd20b2 100644
--- a/packages/packages.go
+++ b/packages/packages.go
@@ -62,6 +62,38 @@ var (
 	ptyrunner = util.CommandRunner(&ptyRunner{})
 )
 
+// PackageUpdatesProvider define contract to extract available updates from the VM.
+type PackageUpdatesProvider interface {
+	GetPackageUpdates(context.Context) (Packages, error)
+}
+
+// InstalledPackagesProvider define contract to extract installed packages from the VM.
+type InstalledPackagesProvider interface {
+	GetInstalledPackages(context.Context) (Packages, error)
+}
+
+type defaultUpdatesProvider struct{}
+
+// NewPackageUpdatesProvider return fully initialize provider.
+func NewPackageUpdatesProvider() PackageUpdatesProvider {
+	return defaultUpdatesProvider{}
+}
+
+func (p defaultUpdatesProvider) GetPackageUpdates(ctx context.Context) (Packages, error) {
+	return GetPackageUpdates(ctx)
+}
+
+type defaultInstalledPackagesProvider struct{}
+
+// NewInstalledPackagesProvider returns fully initialized provider.
+func NewInstalledPackagesProvider() InstalledPackagesProvider {
+	return defaultInstalledPackagesProvider{}
+}
+
+func (p defaultInstalledPackagesProvider) GetInstalledPackages(ctx context.Context) (Packages, error) {
+	return GetInstalledPackages(ctx)
+}
+
 // Packages is a selection of packages based on their manager.
 type Packages struct {
 	Yum                []*PkgInfo            `json:"yum,omitempty"`
diff --git a/packages/packages_linux.go b/packages/packages_linux.go
index 3b7c99405..265dde738 100644
--- a/packages/packages_linux.go
+++ b/packages/packages_linux.go
@@ -25,7 +25,7 @@ import (
 
 // GetPackageUpdates gets all available package updates from any known
 // installed package manager.
-func GetPackageUpdates(ctx context.Context) (*Packages, error) {
+func GetPackageUpdates(ctx context.Context) (Packages, error) {
 	pkgs, errs := getPackageUpdates(ctx)
 
 	var err error
@@ -36,8 +36,9 @@ func GetPackageUpdates(ctx context.Context) (*Packages, error) {
 	return pkgs, err
 }
 
-func getPackageUpdates(ctx context.Context) (*Packages, []string) {
+func getPackageUpdates(ctx context.Context) (Packages, []string) {
 	pkgs := Packages{}
+
 	var errs []string
 	if AptExists {
 		apt, err := AptUpdates(ctx, AptGetUpgradeType(AptGetFullUpgrade), AptGetUpgradeShowNew(false))
@@ -96,12 +97,12 @@ func getPackageUpdates(ctx context.Context) (*Packages, []string) {
 		}
 	}
 
-	return &pkgs, errs
+	return pkgs, errs
 }
 
 // GetInstalledPackages gets all installed packages from any known installed
 // package manager.
-func GetInstalledPackages(ctx context.Context) (*Packages, error) {
+func GetInstalledPackages(ctx context.Context) (Packages, error) {
 	pkgs, errs := getInstalledPackages(ctx)
 	var err error
 	if len(errs) != 0 {
@@ -111,8 +112,9 @@ func GetInstalledPackages(ctx context.Context) (*Packages, error) {
 	return pkgs, err
 }
 
-func getInstalledPackages(ctx context.Context) (*Packages, []string) {
-	pkgs := &Packages{}
+func getInstalledPackages(ctx context.Context) (Packages, []string) {
+	pkgs := Packages{}
+
 	var errs []string
 	if RPMQueryExists {
 		rpm, err := InstalledRPMPackages(ctx)
diff --git a/packages/packages_windows.go b/packages/packages_windows.go
index f804acb0e..0f558f739 100644
--- a/packages/packages_windows.go
+++ b/packages/packages_windows.go
@@ -61,7 +61,7 @@ func wuaUpdates(ctx context.Context, query string) ([]*WUAPackage, error) {
 
 // GetPackageUpdates gets available package updates GooGet as well as any
 // available updates from Windows Update Agent.
-func GetPackageUpdates(ctx context.Context) (*Packages, error) {
+func GetPackageUpdates(ctx context.Context) (Packages, error) {
 	var pkgs Packages
 	var errs []string
 
@@ -89,12 +89,12 @@ func GetPackageUpdates(ctx context.Context) (*Packages, error) {
 	if len(errs) != 0 {
 		err = errors.New(strings.Join(errs, "\n"))
 	}
-	return &pkgs, err
+	return pkgs, err
 }
 
 // GetInstalledPackages gets all installed GooGet packages and Windows updates.
 // Windows updates are read from Windows Update Agent and Win32_QuickFixEngineering.
-func GetInstalledPackages(ctx context.Context) (*Packages, error) {
+func GetInstalledPackages(ctx context.Context) (Packages, error) {
 	var pkgs Packages
 	var errs []string
 
@@ -139,5 +139,5 @@ func GetInstalledPackages(ctx context.Context) (*Packages, error) {
 	if len(errs) != 0 {
 		err = errors.New(strings.Join(errs, "\n"))
 	}
-	return &pkgs, err
+	return pkgs, err
 }


Reviews:
user: login: 1pnq
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T15:26:14Z
user: login: 1pnq
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T15:39:00Z
user: login: 1pnq
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T15:39:09Z
user: login: 1pnq
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T16:04:38Z
user: login: 1pnq
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T16:06:40Z
user: login: burov
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T13:26:44Z
user: login: burov
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T13:27:37Z
user: login: 1pnq
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T14:37:47Z

Review comments:
diff_hunk: @@ -41,6 +41,59 @@ type InstanceInventory struct {
 	LastUpdated          string
 }
 
+type Provider interface {
+	Get(context.Context) *InstanceInventory
+}
+
+type defaultInventoryProvider struct {
+	osInfoProvider osinfo.Provider
+
+	packageUpdatesProvider packages.PackageUpdatesProvider
+	installedPackagesProvider packages.InstalledPackagesProvider
+}
+
+func NewInventoryProvider() Provider {
+	return &defaultInventoryProvider{
+		osInfoProvider: osinfo.NewProvider(),
+		packageUpdatesProvider: packages.DefaultUpdatesProvider{},
+		installedPackagesProvider: packages.DefaultInstalledPackagesProvider{},
+	}
+}
+
+func (p *defaultInventoryProvider) Get(ctx context.Context) *InstanceInventory {
+	clog.Debugf(ctx, "Gathering instance inventory.")
+
+	installedPackages, err := p.installedPackagesProvider.GetInstalledPackages(ctx)
+	if err != nil {
+		clog.Errorf(ctx, "packages.GetInstalledPackages() error: %v", err)
+	}
+
+	packageUpdates, err := p.packageUpdatesProvider.GetPackageUpdates(ctx)
+	if err != nil {
+		clog.Errorf(ctx, "packages.GetPackageUpdates() error: %v", err)
+	}
+
+	oi, err := p.osInfoProvider.Get(ctx)
+	if err != nil {
+		clog.Errorf(ctx, "osinfo.Get() error: %v", err)
+	}
+
+	return &InstanceInventory{
+		Hostname:             oi.Hostname,
+		LongName:             oi.LongName,
+		ShortName:            oi.ShortName,
+		Version:              oi.Version,
+		KernelVersion:        oi.KernelVersion,
+		KernelRelease:        oi.KernelRelease,
+		Architecture:         oi.Architecture,
+		OSConfigAgentVersion: agentconfig.Version(),
+		InstalledPackages:    installedPackages,
+		PackageUpdates:       packageUpdates,
+		LastUpdated:          time.Now().UTC().Format(time.RFC3339),
+	}
+}
+
+
 // Get generates inventory data.
 func Get(ctx context.Context) *InstanceInventory {
path: inventory/inventory.go
user: login: 1pnq
body: unused
created_at: 2025-05-07T15:26:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -62,6 +62,26 @@ var (
 	ptyrunner = util.CommandRunner(&ptyRunner{})
 )
 
+type PackageUpdatesProvider interface {
+	GetPackageUpdates(context.Context) (*Packages, error)
path: packages/packages.go
user: login: 1pnq
body: `Get`
created_at: 2025-05-07T15:38:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -62,6 +62,26 @@ var (
 	ptyrunner = util.CommandRunner(&ptyRunner{})
 )
 
+type PackageUpdatesProvider interface {
+	GetPackageUpdates(context.Context) (*Packages, error)
+}
+
+type DefaultUpdatesProvider struct{}
+
+func (p DefaultUpdatesProvider) Get(ctx context.Context) (*Packages, error) {
+	return GetPackageUpdates(ctx)
+}
+
+type InstalledPackagesProvider interface {
+	GetInstalledPackages(context.Context) (*Packages, error)
path: packages/packages.go
user: login: 1pnq
body: `Get`
created_at: 2025-05-07T15:39:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -28,7 +28,24 @@ const (
 
 // Provider is an interface for OSInfo extraction on different systems.
 type Provider interface {
-	Get(context.Context) (OSInfo, error)
+	Get(context.Context) (*OSInfo, error)
+}
+
+// NewProvider returns fully function provider.
+func NewProvider() Provider {
+	return defaultProvider{}
+}
+
+type defaultProvider struct{}
+
+// Get extract return OSInfo for current platform.
path: osinfo/osinfo.go
user: login: 1pnq
body: extract vs return

btw, can func body be replaced with `return Get()`?
created_at: 2025-05-07T16:04:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -41,6 +41,59 @@ type InstanceInventory struct {
 	LastUpdated          string
 }
 
+type Provider interface {
+	Get(context.Context) *InstanceInventory
+}
+
+type defaultInventoryProvider struct {
+	osInfoProvider osinfo.Provider
+
+	packageUpdatesProvider packages.PackageUpdatesProvider
+	installedPackagesProvider packages.InstalledPackagesProvider
+}
+
+func NewInventoryProvider() Provider {
+	return &defaultInventoryProvider{
+		osInfoProvider: osinfo.NewProvider(),
+		packageUpdatesProvider: packages.DefaultUpdatesProvider{},
+		installedPackagesProvider: packages.DefaultInstalledPackagesProvider{},
+	}
+}
+
+func (p *defaultInventoryProvider) Get(ctx context.Context) *InstanceInventory {
+	clog.Debugf(ctx, "Gathering instance inventory.")
+
+	installedPackages, err := p.installedPackagesProvider.GetInstalledPackages(ctx)
+	if err != nil {
+		clog.Errorf(ctx, "packages.GetInstalledPackages() error: %v", err)
+	}
+
+	packageUpdates, err := p.packageUpdatesProvider.GetPackageUpdates(ctx)
+	if err != nil {
+		clog.Errorf(ctx, "packages.GetPackageUpdates() error: %v", err)
+	}
+
+	oi, err := p.osInfoProvider.Get(ctx)
+	if err != nil {
+		clog.Errorf(ctx, "osinfo.Get() error: %v", err)
+	}
+
+	return &InstanceInventory{
+		Hostname:             oi.Hostname,
+		LongName:             oi.LongName,
+		ShortName:            oi.ShortName,
+		Version:              oi.Version,
+		KernelVersion:        oi.KernelVersion,
+		KernelRelease:        oi.KernelRelease,
+		Architecture:         oi.Architecture,
+		OSConfigAgentVersion: agentconfig.Version(),
+		InstalledPackages:    installedPackages,
+		PackageUpdates:       packageUpdates,
+		LastUpdated:          time.Now().UTC().Format(time.RFC3339),
+	}
+}
+
+
 // Get generates inventory data.
 func Get(ctx context.Context) *InstanceInventory {
path: inventory/inventory.go
user: login: burov
body: done
created_at: 2025-05-08T13:26:44Z
author_association: MEMBER
diff_hunk: @@ -62,6 +62,26 @@ var (
 	ptyrunner = util.CommandRunner(&ptyRunner{})
 )
 
+type PackageUpdatesProvider interface {
+	GetPackageUpdates(context.Context) (*Packages, error)
path: packages/packages.go
user: login: burov
body: resuch approach to the naming will block single structure to implement all interfaces.
created_at: 2025-05-08T13:27:36Z
author_association: MEMBER

PR comments:
user: login: google-oss-prow[bot]
created_at: 2025-05-08T14:37:59Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/GoogleCloudPlatform/osconfig/pull/825#pullrequestreview-2825370806" title="Approved">1pnq</a>*, *<a href="https://github.com/GoogleCloudPlatform/osconfig/pull/825#" title="Author self-approved">burov</a>*

The full list of commands accepted by this bot can be found [here](https://oss.gprow.dev/command-help?repo=GoogleCloudPlatform%2Fosconfig).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/GoogleCloudPlatform/osconfig/blob/master/OWNERS)~~ [burov]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: NONE